diff --git a/internal/app/app.go b/internal/app/app.go index 1b29b60..2672a06 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -5,6 +5,7 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/hedhyw/json-log-viewer/internal/keymap" "github.com/hedhyw/json-log-viewer/internal/pkg/source" "github.com/hedhyw/json-log-viewer/internal/pkg/config" @@ -22,7 +23,7 @@ type Application struct { Entries source.LazyLogEntries Version string - keys KeyMap + keys keymap.KeyMap help help.Model } @@ -49,7 +50,7 @@ func newApplication( }, Version: version, - keys: defaultKeys, + keys: keymap.GetDefaultKeys(), help: help.New(), } } diff --git a/internal/app/helper.go b/internal/app/helper.go index ce0b8f0..88f7320 100644 --- a/internal/app/helper.go +++ b/internal/app/helper.go @@ -15,17 +15,17 @@ import ( ) func (app *Application) getLogLevelStyle( - logEntries source.LazyLogEntries, + renderedRows []table.Row, baseStyle lipgloss.Style, rowID int, ) lipgloss.Style { - if rowID < 0 || rowID >= logEntries.Len() { + if rowID < 0 || rowID >= len(renderedRows) { return baseStyle } - entry := logEntries.Entries[rowID].LogEntry(logEntries.Seeker, app.Config) + row := renderedRows[rowID] - color := getColorForLogLevel(app.getLogLevelFromLogEntry(entry)) + color := getColorForLogLevel(app.getLogLevelFromLogRow(row)) if color == "" { return baseStyle } @@ -62,8 +62,8 @@ func getColorForLogLevel(level source.Level) lipgloss.Color { } } -func (app *Application) getLogLevelFromLogEntry(logEntry source.LogEntry) source.Level { - return source.Level(getFieldByKind(app.Config, config.FieldKindLevel, logEntry)) +func (app *Application) getLogLevelFromLogRow(row table.Row) source.Level { + return source.Level(getCellByKind(app.Config, config.FieldKindLevel, row)) } func (app *Application) handleErrorOccuredMsg(msg events.ErrorOccuredMsg) (tea.Model, tea.Cmd) { @@ -97,13 +97,13 @@ func (app *Application) handleOpenJSONRowRequestedMsg( func (app *Application) handleKeyMsg(msg tea.KeyMsg) tea.Cmd { switch { - case key.Matches(msg, defaultKeys.Exit): + case key.Matches(msg, app.keys.Exit): return tea.Quit - case key.Matches(msg, defaultKeys.Filter): + case key.Matches(msg, app.keys.Filter): return events.FilterKeyClicked - case key.Matches(msg, defaultKeys.Open): + case key.Matches(msg, app.keys.Open): return events.EnterKeyClicked - case key.Matches(msg, defaultKeys.ToggleViewArrow): + case key.Matches(msg, app.keys.ToggleViewArrow): return events.ArrowRightKeyClicked default: return nil @@ -151,24 +151,31 @@ func removeClearSequence(value string) string { return strings.ReplaceAll(value, "\x1b[0", "\x1b[39") } -func getFieldByKind( +func getCellByKind( cfg *config.Config, kind config.FieldKind, - logEntry source.LogEntry, + row table.Row, ) string { - for i, f := range cfg.Fields { - if f.Kind != kind { - continue - } + index := getIndexByKind(cfg, kind) - if i >= len(logEntry.Fields) { - return "-" - } + if index < 0 || index >= len(row) { + return "-" + } + + return row[index] +} - return logEntry.Fields[i] +func getIndexByKind( + cfg *config.Config, + kind config.FieldKind, +) int { + for i, f := range cfg.Fields { + if f.Kind == kind { + return i + } } - return "" + return -1 } func batched[T any](m T, cmd tea.Cmd) func(batch []tea.Cmd) (T, []tea.Cmd) { diff --git a/internal/app/keymap.go b/internal/app/keymap.go deleted file mode 100644 index bc1e993..0000000 --- a/internal/app/keymap.go +++ /dev/null @@ -1,80 +0,0 @@ -package app - -import "github.com/charmbracelet/bubbles/key" - -type KeyMap struct { - Exit key.Binding - Back key.Binding - Open key.Binding - ToggleViewArrow key.Binding - Up key.Binding - Reverse key.Binding - Down key.Binding - Filter key.Binding - ToggleFullHelp key.Binding - GotoTop key.Binding - GotoBottom key.Binding - ShowPreview key.Binding -} - -var defaultKeys = KeyMap{ - Exit: key.NewBinding( - key.WithKeys("ctrl+c", "f10"), - key.WithHelp("Ctrl+C", "Exit"), - ), - Back: key.NewBinding( - key.WithKeys("esc", "q"), - key.WithHelp("esc", "Back"), - ), - Open: key.NewBinding( - key.WithKeys("enter"), - key.WithHelp("enter", "Open"), - ), - ToggleViewArrow: key.NewBinding( - key.WithKeys("right"), - ), - Up: key.NewBinding( - key.WithKeys("up"), - key.WithHelp("↑", "Up"), - ), - Reverse: key.NewBinding( - key.WithKeys("r"), - key.WithHelp("r", "Reverse"), - ), - Down: key.NewBinding( - key.WithKeys("down"), - key.WithHelp("↓", "Down"), - ), - Filter: key.NewBinding( - key.WithKeys("f"), - key.WithHelp("f", "Filter"), - ), - ToggleFullHelp: key.NewBinding( - key.WithKeys("?"), - key.WithHelp("?", "Help"), - ), - GotoTop: key.NewBinding( - key.WithKeys("home"), - key.WithHelp("home", "go to start"), - ), - GotoBottom: key.NewBinding( - key.WithKeys("end", "G"), - key.WithHelp("end", "go to end"), - ), -} - -func (k KeyMap) ShortHelp() []key.Binding { - return []key.Binding{ - k.Back, k.Open, k.Up, k.Down, k.ToggleFullHelp, - } -} - -func (k KeyMap) FullHelp() [][]key.Binding { - return [][]key.Binding{ - {k.Up, k.Down}, - {k.Back, k.Open}, - {k.Filter, k.Reverse}, - {k.GotoTop, k.GotoBottom}, - {k.ToggleFullHelp, k.Exit}, - } -} diff --git a/internal/app/lazytable.go b/internal/app/lazytable.go index b44b499..6a86ed3 100644 --- a/internal/app/lazytable.go +++ b/internal/app/lazytable.go @@ -8,13 +8,17 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/hedhyw/json-log-viewer/internal/pkg/config" + "github.com/hedhyw/json-log-viewer/internal/pkg/source" ) // rowGetter renders the row. type rowGetter interface { // Row return a rendered table row. Row(cfg *config.Config, i int) table.Row + // Len returns the number of all rows. Len() int + // LogEntry getter + LogEntry(cfg *config.Config, i int) source.LogEntry } // lazyTableModel lazily renders table rows. @@ -67,6 +71,27 @@ func (m lazyTableModel) Update(msg tea.Msg) (lazyTableModel, tea.Cmd) { return m, cmd } +func (m lazyTableModel) getCellRenderer() func(table.Model, string, table.CellPosition) string { + cellIDLogLevel := getIndexByKind(m.Config, config.FieldKindLevel) + tableStyles := getTableStyles() + + return func(_ table.Model, value string, position table.CellPosition) string { + style := tableStyles.Cell + + if position.Column == cellIDLogLevel { + return removeClearSequence( + m.Application.getLogLevelStyle( + m.renderedRows, + style, + position.RowID, + ).Render(value), + ) + } + + return style.Render(value) + } +} + func (m lazyTableModel) handleKey(msg tea.KeyMsg, render bool) (lazyTableModel, bool) { // toggle the reverse display of items. if key.Matches(msg, m.Application.keys.Reverse) { @@ -170,13 +195,16 @@ func (m lazyTableModel) RenderedRows() lazyTableModel { } end := min(m.offset+m.table.Height(), m.entries.Len()) - m.renderedRows = []table.Row{} + m.renderedRows = m.renderedRows[:0] + renderedEntries := make([]source.LogEntry, 0, cap(m.renderedRows)) for i := m.offset; i < end; i++ { m.renderedRows = append(m.renderedRows, m.entries.Row(m.Config, i)) + renderedEntries = append(renderedEntries, m.entries.LogEntry(m.Config, i)) } if m.reverse { slices.Reverse(m.renderedRows) + slices.Reverse(renderedEntries) } m.table.SetRows(m.renderedRows) @@ -190,5 +218,9 @@ func (m lazyTableModel) RenderedRows() lazyTableModel { m.lastCursor = m.table.Cursor() + tableStyles := getTableStyles() + tableStyles.RenderCell = m.getCellRenderer() + m.table.SetStyles(tableStyles) + return m } diff --git a/internal/app/logstable.go b/internal/app/logstable.go index ddd980c..ca53b92 100644 --- a/internal/app/logstable.go +++ b/internal/app/logstable.go @@ -20,8 +20,6 @@ type logsTableModel struct { } func newLogsTableModel(application *Application, logEntries source.LazyLogEntries) logsTableModel { - const cellIDLogLevel = 1 - tableLogs := table.New( table.WithColumns(getColumns(application.LastWindowSize.Width, application.Config)), table.WithFocused(true), @@ -34,25 +32,6 @@ func newLogsTableModel(application *Application, logEntries source.LazyLogEntrie tableLogs.SetStyles(getTableStyles()) - tableStyles := getTableStyles() - tableStyles.RenderCell = func(_ table.Model, value string, position table.CellPosition) string { - style := tableStyles.Cell - - if position.Column == cellIDLogLevel { - return removeClearSequence( - application.getLogLevelStyle( - logEntries, - style, - position.RowID, - ).Render(value), - ) - } - - return style.Render(value) - } - - tableLogs.SetStyles(tableStyles) - lazyTable := lazyTableModel{ Application: application, reverse: true, diff --git a/internal/app/statefiltering.go b/internal/app/statefiltering.go index 31aaf66..b5fd0c0 100644 --- a/internal/app/statefiltering.go +++ b/internal/app/statefiltering.go @@ -5,6 +5,7 @@ import ( "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" ) @@ -16,7 +17,7 @@ type StateFilteringModel struct { table logsTableModel textInput textinput.Model - keys KeyMap + keys keymap.KeyMap } func newStateFiltering( @@ -32,7 +33,7 @@ func newStateFiltering( table: previousState.table, textInput: textInput, - keys: defaultKeys, + keys: previousState.getApplication().keys, } } diff --git a/internal/app/stateviewrow.go b/internal/app/stateviewrow.go index 87a7eac..941e595 100644 --- a/internal/app/stateviewrow.go +++ b/internal/app/stateviewrow.go @@ -3,6 +3,7 @@ package app import ( tea "github.com/charmbracelet/bubbletea" + "github.com/hedhyw/json-log-viewer/internal/keymap" "github.com/hedhyw/json-log-viewer/internal/pkg/events" "github.com/hedhyw/json-log-viewer/internal/pkg/source" "github.com/hedhyw/json-log-viewer/internal/pkg/widgets" @@ -18,17 +19,23 @@ type StateViewRowModel struct { logEntry source.LogEntry jsonView tea.Model - keys KeyMap + keys keymap.KeyMap } func newStateViewRow( logEntry source.LogEntry, previousState stateModel, ) StateViewRowModel { - jsonViewModel, cmd := widgets.NewJSONViewModel(logEntry.Line, previousState.getApplication().LastWindowSize) + app := previousState.getApplication() + + jsonViewModel, cmd := widgets.NewJSONViewModel( + logEntry.Line, + app.LastWindowSize, + app.keys, + ) return StateViewRowModel{ - Application: previousState.getApplication(), + Application: app, previousState: previousState, initCmd: cmd, @@ -36,7 +43,7 @@ func newStateViewRow( logEntry: logEntry, jsonView: jsonViewModel, - keys: defaultKeys, + keys: previousState.getApplication().keys, } } diff --git a/internal/keymap/keymap.go b/internal/keymap/keymap.go new file mode 100644 index 0000000..049518b --- /dev/null +++ b/internal/keymap/keymap.go @@ -0,0 +1,84 @@ +package keymap + +import "github.com/charmbracelet/bubbles/key" + +// KeyMap of the app. +type KeyMap struct { + Exit key.Binding + Back key.Binding + Open key.Binding + ToggleViewArrow key.Binding + Up key.Binding + Reverse key.Binding + Down key.Binding + Filter key.Binding + ToggleFullHelp key.Binding + GotoTop key.Binding + GotoBottom key.Binding + ShowPreview key.Binding +} + +// GetDefaultKeys returns default KeyMap. +func GetDefaultKeys() KeyMap { + return KeyMap{ + Exit: key.NewBinding( + key.WithKeys("ctrl+c", "f10"), + key.WithHelp("Ctrl+C", "Exit"), + ), + Back: key.NewBinding( + key.WithKeys("esc", "q"), + key.WithHelp("esc", "Back"), + ), + Open: key.NewBinding( + key.WithKeys("enter"), + key.WithHelp("enter", "Open"), + ), + ToggleViewArrow: key.NewBinding( + key.WithKeys("right"), + ), + Up: key.NewBinding( + key.WithKeys("up"), + key.WithHelp("↑", "Up"), + ), + Reverse: key.NewBinding( + key.WithKeys("r"), + key.WithHelp("r", "Reverse"), + ), + Down: key.NewBinding( + key.WithKeys("down"), + key.WithHelp("↓", "Down"), + ), + Filter: key.NewBinding( + key.WithKeys("f"), + key.WithHelp("f", "Filter"), + ), + ToggleFullHelp: key.NewBinding( + key.WithKeys("?"), + key.WithHelp("?", "Help"), + ), + GotoTop: key.NewBinding( + key.WithKeys("home"), + key.WithHelp("home", "go to start"), + ), + GotoBottom: key.NewBinding( + key.WithKeys("end", "G"), + key.WithHelp("end", "go to end"), + ), + } +} + +func (k KeyMap) ShortHelp() []key.Binding { + return []key.Binding{ + k.Back, k.Open, k.Up, k.Down, k.ToggleFullHelp, + } +} + +func (k KeyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + {k.Up, k.Down}, + {k.Back, k.Open}, + {k.Filter, k.Reverse}, + {k.GotoTop, k.GotoBottom}, + {k.ToggleFullHelp, k.Exit}, + } +} diff --git a/internal/pkg/source/entry.go b/internal/pkg/source/entry.go index 7c67e00..e610eb1 100644 --- a/internal/pkg/source/entry.go +++ b/internal/pkg/source/entry.go @@ -77,9 +77,15 @@ type LazyLogEntries struct { // Row returns table.Row representation of the log entry. func (entries LazyLogEntries) Row(cfg *config.Config, i int) table.Row { - return entries.Entries[i].LogEntry(entries.Seeker, cfg).Fields + return entries.LogEntry(cfg, i).Fields } +// LogEntry getter. +func (entries LazyLogEntries) LogEntry(cfg *config.Config, i int) LogEntry { + return entries.Entries[i].LogEntry(entries.Seeker, cfg) +} + +// Len return the number of all entries. func (entries LazyLogEntries) Len() int { return len(entries.Entries) } diff --git a/internal/pkg/widgets/jsonview.go b/internal/pkg/widgets/jsonview.go index ddbe6e0..aa10688 100644 --- a/internal/pkg/widgets/jsonview.go +++ b/internal/pkg/widgets/jsonview.go @@ -5,6 +5,8 @@ import ( fx "github.com/antonmedv/fx/pkg/model" tea "github.com/charmbracelet/bubbletea" + + "github.com/hedhyw/json-log-viewer/internal/keymap" ) const themeFX = "1" @@ -16,13 +18,17 @@ func init() { // NewJSONViewModel creates a new JSON view widget if a content is the correct json, // or plain text view otherwise. -func NewJSONViewModel(content []byte, lastWindowSize tea.WindowSizeMsg) (tea.Model, tea.Cmd) { +func NewJSONViewModel( + content []byte, + lastWindowSize tea.WindowSizeMsg, + keyMap keymap.KeyMap, +) (tea.Model, tea.Cmd) { fxModel, err := fx.New(fx.Config{ FileName: "", Source: bytes.NewReader(content), }) if err != nil { - return NewPlainLogModel(string(content), lastWindowSize) + return NewPlainLogModel(string(content), lastWindowSize, keyMap) } return fxModel.Update(lastWindowSize) diff --git a/internal/pkg/widgets/jsonview_test.go b/internal/pkg/widgets/jsonview_test.go index 71ee6e7..8d52384 100644 --- a/internal/pkg/widgets/jsonview_test.go +++ b/internal/pkg/widgets/jsonview_test.go @@ -3,6 +3,7 @@ package widgets_test import ( "testing" + "github.com/hedhyw/json-log-viewer/internal/keymap" "github.com/hedhyw/json-log-viewer/internal/pkg/widgets" "github.com/stretchr/testify/assert" @@ -14,7 +15,11 @@ func TestNewJSONViewModel(t *testing.T) { t.Run("plain_text", func(t *testing.T) { t.Parallel() - model, _ := widgets.NewJSONViewModel([]byte(text), getFakeTeaWindowSizeMsg()) + model, _ := widgets.NewJSONViewModel( + []byte(text), + getFakeTeaWindowSizeMsg(), + keymap.GetDefaultKeys(), + ) _, ok := model.(widgets.PlainLogModel) assert.Truef(t, ok, "actual type: %T", model) @@ -26,6 +31,7 @@ func TestNewJSONViewModel(t *testing.T) { model, _ := widgets.NewJSONViewModel( []byte(`{"hello":"world"}`), getFakeTeaWindowSizeMsg(), + keymap.GetDefaultKeys(), ) _, ok := model.(widgets.PlainLogModel) diff --git a/internal/pkg/widgets/plain.go b/internal/pkg/widgets/plain.go index d86b170..c50911c 100644 --- a/internal/pkg/widgets/plain.go +++ b/internal/pkg/widgets/plain.go @@ -1,15 +1,19 @@ package widgets import ( + "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/muesli/reflow/wordwrap" + + "github.com/hedhyw/json-log-viewer/internal/keymap" ) // PlainLogModel is a widget that shows multiline text in a viewport. type PlainLogModel struct { viewport viewport.Model text string + keyMap keymap.KeyMap } // NewPlainLogModel initializes a new PlainLogModel with the given text. @@ -17,10 +21,12 @@ type PlainLogModel struct { func NewPlainLogModel( text string, windowSize tea.WindowSizeMsg, + keyMap keymap.KeyMap, ) (PlainLogModel, tea.Cmd) { m := PlainLogModel{ text: text, viewport: viewport.New(windowSize.Width, windowSize.Height), + keyMap: keyMap, } m = m.refreshText(windowSize.Width) @@ -47,6 +53,12 @@ func (m PlainLogModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.viewport.Width = msg.Width m.viewport.Height = msg.Height m = m.refreshText(msg.Width) + case tea.KeyMsg: + if key.Matches(msg, m.keyMap.Exit) || + key.Matches(msg, m.keyMap.Back) || + key.Matches(msg, m.keyMap.Open) { + return m, tea.Quit + } } var cmd tea.Cmd diff --git a/internal/pkg/widgets/plain_test.go b/internal/pkg/widgets/plain_test.go index 93b4f25..a7f2383 100644 --- a/internal/pkg/widgets/plain_test.go +++ b/internal/pkg/widgets/plain_test.go @@ -6,13 +6,14 @@ import ( tea "github.com/charmbracelet/bubbletea" "github.com/stretchr/testify/assert" + "github.com/hedhyw/json-log-viewer/internal/keymap" "github.com/hedhyw/json-log-viewer/internal/pkg/widgets" ) const text = "hello world" func TestPlainLogModelInit(t *testing.T) { - model, _ := widgets.NewPlainLogModel(text, getFakeTeaWindowSizeMsg()) + model, _ := widgets.NewPlainLogModel(text, getFakeTeaWindowSizeMsg(), keymap.GetDefaultKeys()) cmd := model.Init() assert.Nil(t, cmd) @@ -20,7 +21,7 @@ func TestPlainLogModelInit(t *testing.T) { func TestPlainLogModelUpdateTeaWindowSizeMsg(t *testing.T) { windowSize := getFakeTeaWindowSizeMsg() - model, _ := widgets.NewPlainLogModel(text, windowSize) + model, _ := widgets.NewPlainLogModel(text, windowSize, keymap.GetDefaultKeys()) windowSize.Height++ windowSize.Width++ @@ -32,7 +33,7 @@ func TestPlainLogModelUpdateTeaWindowSizeMsg(t *testing.T) { } func TestPlainLogModelView(t *testing.T) { - model, _ := widgets.NewPlainLogModel(text, getFakeTeaWindowSizeMsg()) + model, _ := widgets.NewPlainLogModel(text, getFakeTeaWindowSizeMsg(), keymap.GetDefaultKeys()) actual := model.View() assert.Contains(t, actual, text)