Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
| Enter | Open log |
| Esc | Back |
| F | Filter |
| / | RegExp Filter |
| R | Reverse |
| Ctrl+C | Exit |
| F10 | Exit |
Expand Down
6 changes: 6 additions & 0 deletions internal/app/stateloaded.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ func (s StateLoadedModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return s, tea.Quit
case key.Matches(msg, s.keys.Filter):
return s.handleFilterKeyClickedMsg()
case key.Matches(msg, s.keys.FilterRegex):
Copy link
Owner

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.

return s.handleRegexFilterKeyClickedMsg()
case key.Matches(msg, s.keys.ToggleViewArrow), key.Matches(msg, s.keys.Open):
return s.handleRequestOpenJSON()
case key.Matches(msg, s.keys.ToggleFullHelp):
Expand Down Expand Up @@ -153,6 +155,10 @@ func (s StateLoadedModel) handleFilterKeyClickedMsg() (tea.Model, tea.Cmd) {
return initializeModel(newStateFiltering(s))
}

func (s StateLoadedModel) handleRegexFilterKeyClickedMsg() (tea.Model, tea.Cmd) {
return initializeModel(newStateRegexFiltering(s))
}

func (s StateLoadedModel) getApplication() *Application {
return s.Application
}
Expand Down
135 changes: 135 additions & 0 deletions internal/app/stateregexfiltered.go
Copy link
Owner

Choose a reason for hiding this comment

The 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 - StateRegexFilteredModel (as you implemented). However it introduces a lot of boilerplate and still overlaps with StateFilteredModel. What about extending the latter? newStateFiltered -> newStateFilteredByText(previousState, filterText) and newStateFilteredByRegexp(previousState, filterRegexp).

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) {
Copy link
Owner

Choose a reason for hiding this comment

The 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)
}
103 changes: 103 additions & 0 deletions internal/app/stateregexfiltering.go
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)
}
6 changes: 6 additions & 0 deletions internal/keymap/keymap.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type KeyMap struct {
PageUp key.Binding
PageDown key.Binding
Filter key.Binding
FilterRegex key.Binding
ToggleFullHelp key.Binding
GotoTop key.Binding
GotoBottom key.Binding
Expand Down Expand Up @@ -63,6 +64,10 @@ func GetDefaultKeys() KeyMap {
key.WithKeys("f"),
key.WithHelp("f", "Filter"),
),
FilterRegex: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("/", "RegExp Filter"),
),
ToggleFullHelp: key.NewBinding(
key.WithKeys("?"),
key.WithHelp("?", "Help"),
Expand Down Expand Up @@ -92,5 +97,6 @@ func (k KeyMap) FullHelp() [][]key.Binding {
{k.PageUp, k.PageDown},
{k.GotoTop, k.GotoBottom},
{k.ToggleFullHelp, k.Exit},
{k.FilterRegex},
}
}
32 changes: 32 additions & 0 deletions internal/pkg/source/entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"encoding/json"
"fmt"
"os"
"regexp"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -117,6 +118,37 @@
}, nil
}

// FilterRegEx filters entries by regex ignoring case

Check failure on line 121 in internal/pkg/source/entry.go

View workflow job for this annotation

GitHub Actions / Test

Comment should end in a period (godot)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// FilterRegEx filters entries by regex ignoring case
// FilterRegEx filters entries by regex ignoring case.

It will fix a linter issue, you can try make lint

func (entries LazyLogEntries) FilterRegExp(regex string) (LazyLogEntries, error) {
if regex == "" {
return entries, nil
}

re, err := regexp.Compile(strings.ToLower(regex))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: there’s a (?i) flag for case-insensitive matching, you just need to add it to the start of the regex if it isn't already there.

(?i)[a-z0-9]+ will match both AAAAA and aaaaaa

Copy link
Owner

Choose a reason for hiding this comment

The 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


Check failure on line 128 in internal/pkg/source/entry.go

View workflow job for this annotation

GitHub Actions / Test

File is not properly formatted (gofumpt)
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))) {

Check failure on line 141 in internal/pkg/source/entry.go

View workflow job for this annotation

GitHub Actions / Test

unnecessary conversion (unconvert)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: we don't need to call ToLower if you include the (?i) flag

filtered = append(filtered, f)
}
}

return LazyLogEntries{
Seeker: entries.Seeker,
Entries: filtered,
}, nil
}

func parseField(
parsedLine any,
field config.Field,
Expand Down
Loading