diff --git a/internal/app/lazytable.go b/internal/app/lazytable.go index e5e6f84..e5e7d53 100644 --- a/internal/app/lazytable.go +++ b/internal/app/lazytable.go @@ -141,9 +141,9 @@ func (m lazyTableModel) handleKey(msg tea.KeyMsg, render bool) (lazyTableModel, return m, render } -func (m lazyTableModel) ViewPortCursor() int { +func (m lazyTableModel) viewPortCursor() int { if m.reverse { - viewSize := m.ViewPortEnd() - m.ViewPortStart() + viewSize := m.viewPortEnd() - m.viewPortStart() return m.offset + (viewSize - 1 - m.table.Cursor()) } @@ -151,14 +151,15 @@ func (m lazyTableModel) ViewPortCursor() int { return m.offset + m.table.Cursor() } -func (m lazyTableModel) ViewPortStart() int { +func (m lazyTableModel) viewPortStart() int { return m.offset } -func (m lazyTableModel) ViewPortEnd() int { +func (m lazyTableModel) viewPortEnd() int { return min(m.offset+m.table.Height(), m.entries.Len()) } +// RenderedRows returns current visible rendered rows. func (m lazyTableModel) RenderedRows() lazyTableModel { if m.follow { m.offset = max(0, m.entries.Len()-m.table.Height()) diff --git a/internal/app/lazytable_test.go b/internal/app/lazytable_test.go new file mode 100644 index 0000000..4618821 --- /dev/null +++ b/internal/app/lazytable_test.go @@ -0,0 +1,159 @@ +package app_test + +import ( + "strconv" + "strings" + "testing" + + tea "github.com/charmbracelet/bubbletea" + "github.com/stretchr/testify/assert" +) + +func TestLazyTableModelManyRows(t *testing.T) { + t.Parallel() + + const ( + prefix = "n" + countLines = 2_000 + ) + + content := make([]byte, 0, countLines) + + for i := range countLines { + content = append(content, prefix+strconv.Itoa(i)...) + content = append(content, '\n') + } + + model := newTestModel(t, content) + + step := strings.Count(model.View(), prefix) + + for i := countLines - 1; i > countLines-step; i-- { + expectedMessage := prefix + strconv.Itoa(i) + assert.Contains(t, model.View(), expectedMessage) + model = handleUpdate(model, tea.KeyMsg{Type: tea.KeyDown}) + } + + for i := countLines - step - 1; i >= 0; i-- { + model = handleUpdate(model, tea.KeyMsg{Type: tea.KeyDown}) + expectedMessage := prefix + strconv.Itoa(i) + assert.Contains(t, model.View(), expectedMessage) + } +} + +func TestLazyTableModelReverse(t *testing.T) { + t.Parallel() + + const ( + start = "START" + end = "END" + keywordReverse = "reverse" + ) + + var ( + keyReverse = tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'r'}} + keyGoToBottom = tea.KeyMsg{Type: tea.KeyEnd} + keyGoToUp = tea.KeyMsg{Type: tea.KeyHome} + + middle = strings.Repeat("-\n", 100) + content = []byte(start + "\n" + middle + end + "\n") + ) + + t.Run("forward", func(t *testing.T) { + t.Parallel() + + model := newTestModel(t, content) + + model = handleUpdate(model, keyReverse) + + view := model.View() + + // "END" should be at the first half of the screen. + assert.NotContains(t, view, keywordReverse) + assert.Greater(t, strings.Index(view, end), (len(view) / 2), view) + }) + + t.Run("reverse", func(t *testing.T) { + t.Parallel() + + model := newTestModel(t, content) + + model = handleUpdate(model, keyReverse) + model = handleUpdate(model, keyReverse) + + view := model.View() + + // "END" should be at the second half of the screen. + assert.Less(t, strings.Index(view, end), (len(view) / 2), view) + assert.Contains(t, view, keywordReverse) + }) + + t.Run("reverse_default", func(t *testing.T) { + t.Parallel() + + model := newTestModel(t, content) + view := model.View() + assert.Contains(t, view, keywordReverse) + }) + + t.Run("reverse_go_to_bottom", func(t *testing.T) { + t.Parallel() + + model := newTestModel(t, content) + view := model.View() + assert.Contains(t, view, keywordReverse) + + model = handleUpdate(model, keyGoToBottom) + + view = model.View() + + // "START" should be at the second half of the screen. + assert.Greater(t, strings.Index(view, start), (len(view) / 2), view) + }) + + t.Run("reverse_go_to_bottom_and_up", func(t *testing.T) { + t.Parallel() + + model := newTestModel(t, content) + view := model.View() + assert.Contains(t, view, keywordReverse) + + model = handleUpdate(model, keyGoToBottom) + model = handleUpdate(model, keyGoToUp) + + view = model.View() + + assert.Contains(t, view, end) + }) + + t.Run("forwards_go_to_up_and_bottom", func(t *testing.T) { + t.Parallel() + + model := newTestModel(t, content) + view := model.View() + assert.Contains(t, view, keywordReverse) + + model = handleUpdate(model, keyReverse) + model = handleUpdate(model, keyGoToUp) + model = handleUpdate(model, keyGoToBottom) + + view = model.View() + + assert.Contains(t, view, end) + }) + + t.Run("forward_go_to_bottom", func(t *testing.T) { + t.Parallel() + + model := newTestModel(t, content) + view := model.View() + assert.Contains(t, view, keywordReverse) + + model = handleUpdate(model, keyGoToUp) + + view = model.View() + + // "START" should be at the second half of the screen. + assert.Less(t, strings.Index(view, start), (len(view) / 2), view) + }) +} diff --git a/internal/app/logstable.go b/internal/app/logstable.go index 6eaff73..ddd980c 100644 --- a/internal/app/logstable.go +++ b/internal/app/logstable.go @@ -116,5 +116,5 @@ func (m logsTableModel) handleWindowSizeMsg(msg tea.WindowSizeMsg) logsTableMode // Cursor returns the index of the selected row. func (m logsTableModel) Cursor() int { - return m.lazyTable.ViewPortCursor() + return m.lazyTable.viewPortCursor() } diff --git a/internal/app/stateviewrow_test.go b/internal/app/stateviewrow_test.go index 20f5093..6a57529 100644 --- a/internal/app/stateviewrow_test.go +++ b/internal/app/stateviewrow_test.go @@ -16,6 +16,7 @@ import ( func TestStateViewRow(t *testing.T) { setup := func(t *testing.T) tea.Model { t.Parallel() + model := newTestModel(t, assets.ExampleJSONLog()) model = handleUpdate(model, tea.KeyMsg{Type: tea.KeyEnter}) _, ok := model.(app.StateViewRowModel) diff --git a/internal/pkg/source/entry.go b/internal/pkg/source/entry.go index 82d6d51..6af9f71 100644 --- a/internal/pkg/source/entry.go +++ b/internal/pkg/source/entry.go @@ -180,6 +180,10 @@ func parseLogEntry( return getPlainLogEntry(line, cfg) } + if _, ok := parsedLine.(map[string]any); !ok { + return getPlainLogEntry(line, cfg) + } + fields := make([]string, 0, len(cfg.Fields)) for _, f := range cfg.Fields { diff --git a/internal/pkg/source/source.go b/internal/pkg/source/source.go index 2a45c06..3836986 100644 --- a/internal/pkg/source/source.go +++ b/internal/pkg/source/source.go @@ -127,7 +127,7 @@ func (s *Source) CanFollow() bool { const ErrFileTruncated semerr.Error = "file truncated" -// ReadLogEntry reads the next ReadLogEntry from the file. +// readLogEntry reads the next LazyLogEntry from the file. func (s *Source) readLogEntry() (LazyLogEntry, error) { for { if s.reader == nil {