-
Notifications
You must be signed in to change notification settings - Fork 12
feat/regex-search-support #153
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thought: it's more flexible to have a dedicated state for regexp filtering - |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| package app | ||
|
|
||
| import ( | ||
| "fmt" | ||
|
|
||
| "github.com/charmbracelet/bubbles/key" | ||
| tea "github.com/charmbracelet/bubbletea" | ||
|
|
||
| "github.com/hedhyw/json-log-viewer/internal/pkg/events" | ||
| "github.com/hedhyw/json-log-viewer/internal/pkg/source" | ||
| ) | ||
|
|
||
| // StateRegexFilteredModel is a state that shows regex-filtered records. | ||
| type StateRegexFilteredModel struct { | ||
| *Application | ||
|
|
||
| previousState StateLoadedModel | ||
| table logsTableModel | ||
| logEntries source.LazyLogEntries | ||
|
|
||
| regexPattern string | ||
| } | ||
|
|
||
| func newStateRegexFiltered( | ||
| previousState StateLoadedModel, | ||
| regexPattern string, | ||
| ) StateRegexFilteredModel { | ||
| return StateRegexFilteredModel{ | ||
| Application: previousState.Application, | ||
|
|
||
| previousState: previousState, | ||
|
|
||
| regexPattern: regexPattern, | ||
| } | ||
| } | ||
|
|
||
| func (s StateRegexFilteredModel) Init() tea.Cmd { | ||
| return func() tea.Msg { | ||
| return &s | ||
| } | ||
| } | ||
|
|
||
| func (s StateRegexFilteredModel) View() string { | ||
| footer := s.FooterStyle.Render( | ||
| fmt.Sprintf("regex filtered %d by: /%s/", s.logEntries.Len(), s.regexPattern), | ||
| ) | ||
|
|
||
| return s.BaseStyle.Render(s.table.View()) + "\n" + footer | ||
| } | ||
|
|
||
| func (s StateRegexFilteredModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: can you please add comments to all exported methods? |
||
| var cmdBatch []tea.Cmd | ||
|
|
||
| s.Application.Update(msg) | ||
|
|
||
| if _, ok := msg.(*StateRegexFilteredModel); ok { | ||
| s, msg = s.handleStateRegexFilteredModel() | ||
| } | ||
|
|
||
| if _, ok := msg.(events.LogEntriesUpdateMsg); ok { | ||
| return s, nil | ||
| } | ||
|
|
||
| switch typedMsg := msg.(type) { | ||
| case events.ErrorOccuredMsg: | ||
| return s.handleErrorOccuredMsg(typedMsg) | ||
| case events.OpenJSONRowRequestedMsg: | ||
| return s.handleOpenJSONRowRequestedMsg(typedMsg, s) | ||
| case tea.KeyMsg: | ||
| if mdl, cmd := s.handleKeyMsg(typedMsg); mdl != nil { | ||
| return mdl, cmd | ||
| } | ||
| } | ||
|
|
||
| s.table, cmdBatch = batched(s.table.Update(msg))(cmdBatch) | ||
|
|
||
| return s, tea.Batch(cmdBatch...) | ||
| } | ||
|
|
||
| func (s StateRegexFilteredModel) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) { | ||
| switch { | ||
| case key.Matches(msg, s.keys.Back): | ||
| return s.previousState.refresh() | ||
| case key.Matches(msg, s.keys.FilterRegex): | ||
| return s.handleRegexFilterKeyClickedMsg() | ||
| case key.Matches(msg, s.keys.ToggleViewArrow), key.Matches(msg, s.keys.Open): | ||
| return s.handleRequestOpenJSON() | ||
| default: | ||
| return nil, nil | ||
| } | ||
| } | ||
|
|
||
| func (s StateRegexFilteredModel) handleStateRegexFilteredModel() (StateRegexFilteredModel, tea.Msg) { | ||
| entries, err := s.Application.Entries().FilterRegExp(s.regexPattern) | ||
| if err != nil { | ||
| return s, events.ShowError(err)() | ||
| } | ||
|
|
||
| s.logEntries = entries | ||
| s.table = newLogsTableModel( | ||
| s.Application, | ||
| entries, | ||
| false, // follow. | ||
| s.previousState.table.lazyTable.reverse, | ||
| ) | ||
|
|
||
| return s, nil | ||
| } | ||
|
|
||
| func (s StateRegexFilteredModel) handleRegexFilterKeyClickedMsg() (tea.Model, tea.Cmd) { | ||
| state := newStateRegexFiltering(s.previousState) | ||
| return initializeModel(state) | ||
| } | ||
|
|
||
| func (s StateRegexFilteredModel) handleRequestOpenJSON() (tea.Model, tea.Cmd) { | ||
| if s.logEntries.Len() == 0 { | ||
| return s, events.EscKeyClicked | ||
| } | ||
|
|
||
| return s, events.OpenJSONRowRequested(s.logEntries, s.table.Cursor()) | ||
| } | ||
|
|
||
| func (s StateRegexFilteredModel) getApplication() *Application { | ||
| return s.Application | ||
| } | ||
|
|
||
| func (s StateRegexFilteredModel) refresh() (_ stateModel, cmd tea.Cmd) { | ||
| s.table, cmd = s.table.Update(s.LastWindowSize()) | ||
|
|
||
| return s, cmd | ||
| } | ||
|
|
||
| func (s StateRegexFilteredModel) String() string { | ||
| return modelValue(s) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| package app | ||
|
|
||
| import ( | ||
| "github.com/charmbracelet/bubbles/key" | ||
| "github.com/charmbracelet/bubbles/textinput" | ||
| tea "github.com/charmbracelet/bubbletea" | ||
|
|
||
| "github.com/hedhyw/json-log-viewer/internal/keymap" | ||
| "github.com/hedhyw/json-log-viewer/internal/pkg/events" | ||
| ) | ||
|
|
||
| // StateRegexFilteringModel is a state to prompt for regex filter pattern. | ||
| type StateRegexFilteringModel struct { | ||
| *Application | ||
|
|
||
| previousState StateLoadedModel | ||
| table logsTableModel | ||
|
|
||
| textInput textinput.Model | ||
| keys keymap.KeyMap | ||
| } | ||
|
|
||
| func newStateRegexFiltering( | ||
| previousState StateLoadedModel, | ||
| ) StateRegexFilteringModel { | ||
| textInput := textinput.New() | ||
| textInput.Focus() | ||
| textInput.Placeholder = "Enter regex pattern..." | ||
|
|
||
| return StateRegexFilteringModel{ | ||
| Application: previousState.Application, | ||
|
|
||
| previousState: previousState, | ||
| table: previousState.table, | ||
|
|
||
| textInput: textInput, | ||
| keys: previousState.getApplication().keys, | ||
| } | ||
| } | ||
|
|
||
| func (s StateRegexFilteringModel) Init() tea.Cmd { | ||
| return nil | ||
| } | ||
|
|
||
| func (s StateRegexFilteringModel) View() string { | ||
| return s.BaseStyle.Render(s.table.View()) + "\n" + | ||
| "Regex filter: " + s.textInput.View() | ||
| } | ||
|
|
||
| func (s StateRegexFilteringModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { | ||
| var cmdBatch []tea.Cmd | ||
|
|
||
| s.Application.Update(msg) | ||
|
|
||
| switch msg := msg.(type) { | ||
| case events.ErrorOccuredMsg: | ||
| return s.handleErrorOccuredMsg(msg) | ||
| case tea.KeyMsg: | ||
| if mdl, cmd := s.handleKeyMsg(msg); mdl != nil { | ||
| return mdl, cmd | ||
| } | ||
| default: | ||
| s.table, cmdBatch = batched(s.table.Update(msg))(cmdBatch) | ||
| } | ||
|
|
||
| s.textInput, cmdBatch = batched(s.textInput.Update(msg))(cmdBatch) | ||
|
|
||
| return s, tea.Batch(cmdBatch...) | ||
| } | ||
|
|
||
| func (s StateRegexFilteringModel) handleKeyMsg(msg tea.KeyMsg) (tea.Model, tea.Cmd) { | ||
| switch { | ||
| case key.Matches(msg, s.keys.Back) && string(msg.Runes) != "q": | ||
| return s.previousState.refresh() | ||
| case key.Matches(msg, s.keys.Open): | ||
| return s.handleEnterKeyClickedMsg() | ||
| default: | ||
| return nil, nil | ||
| } | ||
| } | ||
|
|
||
| func (s StateRegexFilteringModel) handleEnterKeyClickedMsg() (tea.Model, tea.Cmd) { | ||
| if s.textInput.Value() == "" { | ||
| return s, events.EscKeyClicked | ||
| } | ||
|
|
||
| return initializeModel(newStateRegexFiltered( | ||
| s.previousState, | ||
| s.textInput.Value(), | ||
| )) | ||
| } | ||
|
|
||
| func (s StateRegexFilteringModel) getApplication() *Application { | ||
| return s.Application | ||
| } | ||
|
|
||
| func (s StateRegexFilteringModel) refresh() (stateModel, tea.Cmd) { | ||
| return s, nil | ||
| } | ||
|
|
||
| func (s StateRegexFilteringModel) String() string { | ||
| return modelValue(s) | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -5,6 +5,7 @@ | |||||
| "encoding/json" | ||||||
| "fmt" | ||||||
| "os" | ||||||
| "regexp" | ||||||
| "strconv" | ||||||
| "strings" | ||||||
| "time" | ||||||
|
|
@@ -117,6 +118,37 @@ | |||||
| }, nil | ||||||
| } | ||||||
|
|
||||||
| // FilterRegEx filters entries by regex ignoring case | ||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
It will fix a linter issue, you can try |
||||||
| func (entries LazyLogEntries) FilterRegExp(regex string) (LazyLogEntries, error) { | ||||||
| if regex == "" { | ||||||
| return entries, nil | ||||||
| } | ||||||
|
|
||||||
| re, err := regexp.Compile(strings.ToLower(regex)) | ||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: there’s a
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: how will errors be reported? It would be annoying to loose everything after a small mistake in a regexp |
||||||
|
|
||||||
| if err != nil { | ||||||
| return LazyLogEntries{}, err | ||||||
| } | ||||||
|
|
||||||
| filtered := make([]LazyLogEntry, 0, len(entries.Entries)) | ||||||
|
|
||||||
| for _, f := range entries.Entries { | ||||||
| line, err := f.Line(entries.Seeker) | ||||||
| if err != nil { | ||||||
| return LazyLogEntries{}, err | ||||||
| } | ||||||
|
|
||||||
| if re.Match([]byte(bytes.ToLower(line))) { | ||||||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: we don't need to call |
||||||
| filtered = append(filtered, f) | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| return LazyLogEntries{ | ||||||
| Seeker: entries.Seeker, | ||||||
| Entries: filtered, | ||||||
| }, nil | ||||||
| } | ||||||
|
|
||||||
| func parseField( | ||||||
| parsedLine any, | ||||||
| field config.Field, | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: let's add the tests.