Skip to content

Commit

Permalink
(feature): log panel fuzzy search (#35)
Browse files Browse the repository at this point in the history
* (feature): log panel fuzzy search

Signed-off-by: Bryce Palmer <everettraven@gmail.com>

* add search highlighting, strict/fuzzy match toggle, and help

Signed-off-by: Bryce Palmer <everettraven@gmail.com>

---------

Signed-off-by: Bryce Palmer <everettraven@gmail.com>
  • Loading branch information
everettraven committed Nov 18, 2023
1 parent 8ac4411 commit a32effc
Show file tree
Hide file tree
Showing 6 changed files with 246 additions and 36 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/charmbracelet/bubbles v0.16.1
github.com/charmbracelet/bubbletea v0.24.2
github.com/charmbracelet/lipgloss v0.7.1
github.com/sahilm/fuzzy v0.1.0
github.com/spf13/cobra v1.7.0
github.com/tidwall/gjson v1.17.0
k8s.io/api v0.28.1
Expand All @@ -17,6 +18,7 @@ require (
)

require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/dlclark/regexp2 v1.4.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/calyptia/go-bubble-table v0.2.1 h1:NWcVRyGCLuP7QIA29uUFSY+IjmWcmUWHjy5J/CPb0Rk=
Expand Down Expand Up @@ -73,6 +75,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8=
Expand Down Expand Up @@ -118,6 +122,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
Expand Down
8 changes: 4 additions & 4 deletions pkg/charm/models/dashboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ func (k DashboardKeyMap) FullHelp() [][]key.Binding {

var DefaultDashboardKeys = DashboardKeyMap{
Help: key.NewBinding(
key.WithKeys("?"),
key.WithHelp("?", "toggle help"),
key.WithKeys("ctrl+h"),
key.WithHelp("ctrl+h", "toggle help"),
),
Quit: key.NewBinding(
key.WithKeys("q", "esc", "ctrl+c"),
key.WithHelp("q, esc, ctrl+c", "quit"),
key.WithKeys("q", "ctrl+c"),
key.WithHelp("q, ctrl+c", "quit"),
),
}

Expand Down
215 changes: 205 additions & 10 deletions pkg/charm/models/panels/logs.go
Original file line number Diff line number Diff line change
@@ -1,59 +1,254 @@
package panels

import (
"fmt"
"strings"
"sync"

"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
"github.com/charmbracelet/bubbles/textinput"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/everettraven/buoy/pkg/charm/styles"
"github.com/sahilm/fuzzy"
)

type LogsKeyMap struct {
Search key.Binding
SubmitSearch key.Binding
QuitSearch key.Binding
ToggleStrict key.Binding
}

// ShortHelp returns keybindings to be shown in the mini help view. It's part
// of the key.Map interface.
func (k LogsKeyMap) ShortHelp() []key.Binding {
return []key.Binding{}
}

// FullHelp returns keybindings for the expanded help view. It's part of the
// key.Map interface.
func (k LogsKeyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{
{k.Search, k.SubmitSearch, k.QuitSearch, k.ToggleStrict},
}
}

var DefaultLogsKeys = LogsKeyMap{
Search: key.NewBinding(
key.WithKeys("/"),
key.WithHelp("/", "open a prompt to search logs"),
),
SubmitSearch: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "submit search prompt"),
),
QuitSearch: key.NewBinding(
key.WithKeys("esc"),
key.WithHelp("esc", "exit search mode"),
),
ToggleStrict: key.NewBinding(
key.WithKeys("ctrl+s"),
key.WithHelp("ctrl+s", "toggle strict search mode"),
),
}

const modeLogs = "logs"
const modeSearching = "searching"
const modeSearched = "searched"

// Logs is a tea.Model implementation
// that represents an item panel
type Logs struct {
viewport viewport.Model
name string
mutex *sync.Mutex
content string
viewport viewport.Model
searchbar textinput.Model
name string
mutex *sync.Mutex
content string
contentUpdated bool
mode string
keys LogsKeyMap
strictSearch bool
}

func NewLogs(name string, viewport viewport.Model) *Logs {
func NewLogs(keys LogsKeyMap, name string) *Logs {
searchbar := textinput.New()
searchbar.Prompt = "> "
searchbar.Placeholder = "search term"
return &Logs{
viewport: viewport,
name: name,
mutex: &sync.Mutex{},
content: "",
viewport: viewport.New(10, 10),
searchbar: searchbar,
name: name,
mutex: &sync.Mutex{},
content: "",
mode: modeLogs,
keys: keys,
}
}

func (m *Logs) Init() tea.Cmd { return nil }

func (m *Logs) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.mutex.Lock()
defer m.mutex.Unlock()
var cmd tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.viewport.Width = msg.Width
m.viewport.Height = msg.Height / 2
case tea.KeyMsg:
switch {
case key.Matches(msg, m.keys.Search):
m.mode = modeSearching
if !m.searchbar.Focused() {
m.searchbar.Focus()
}
m.searchbar.SetValue("")
return m, nil
case key.Matches(msg, m.keys.QuitSearch):
m.mode = modeLogs
if m.searchbar.Focused() {
m.searchbar.Blur()
}
m.viewport.SetContent(wrapLogs(m.content, m.viewport.Width))
m.contentUpdated = false
case key.Matches(msg, m.keys.SubmitSearch):
if m.mode == modeSearching {
m.mode = modeSearched
if m.searchbar.Focused() {
m.searchbar.Blur()
}
}
case key.Matches(msg, m.keys.ToggleStrict):
m.strictSearch = !m.strictSearch
}
}

if m.contentUpdated && m.mode == modeLogs {
m.viewport.SetContent(wrapLogs(m.content, m.viewport.Width))
m.contentUpdated = false
}

if m.mode == modeSearching {
m.searchbar, cmd = m.searchbar.Update(msg)
return m, cmd
}

if m.mode == modeSearched {
m.viewport.SetContent(searchLogs(m.content, m.searchbar.Value(), m.viewport.Width, m.strictSearch))
}

m.viewport, cmd = m.viewport.Update(msg)
return m, cmd
}

func (m *Logs) View() string {
searchMode := "fuzzy"
if m.strictSearch {
searchMode = "strict"
}
searchModeOutput := styles.LogSearchModeStyle().Render(fmt.Sprintf("search mode: %s", searchMode))

if m.mode == modeSearching {
return lipgloss.JoinVertical(lipgloss.Top,
m.searchbar.View(),
searchModeOutput,
)
}

if m.mode == modeSearched {
return lipgloss.JoinVertical(lipgloss.Top,
m.viewport.View(),
searchModeOutput,
)
}

return m.viewport.View()
}

func (m *Logs) Help() help.KeyMap {
return m.keys
}

func (m *Logs) AddContent(content string) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.content = strings.Join([]string{m.content, content}, "\n")
m.viewport.SetContent(wrapLogs(m.content, m.viewport.Width))
m.contentUpdated = true
}

func (m *Logs) Name() string {
return m.name
}

// searchLogs searches the logs for the given term
// and returns a string with the matching log lines
// and the matched term highlighted. Uses fuzzy search
// if strict is false. Wraps logs to the given width if wrap > 0.
func searchLogs(logs, term string, wrap int, strict bool) string {
splitLogs := strings.Split(logs, "\n")
if strict {
return strictMatchLogs(term, splitLogs, wrap)
}
return fuzzyMatchLogs(term, splitLogs, wrap)
}

func strictMatchLogs(searchTerm string, logLines []string, wrap int) string {
var results strings.Builder
for _, log := range logLines {
if wrap > 0 {
log = wrapLogs(log, wrap)
}
if strings.Contains(log, searchTerm) {
highlighted := strings.Replace(
log,
searchTerm,
styles.LogSearchHighlightStyle().Render(searchTerm), -1,
)
results.WriteString(highlighted + "\n")
}
}
return results.String()
}

func fuzzyMatchLogs(searchTerm string, logLines []string, wrap int) string {
var matches []fuzzy.Match
if wrap > 0 {
wrappedLogs := []string{}
for _, log := range logLines {
wrappedLogs = append(wrappedLogs, wrapLogs(log, wrap))
}
matches = fuzzy.Find(searchTerm, wrappedLogs)
} else {
matches = fuzzy.Find(searchTerm, logLines)
}

var results strings.Builder
for _, match := range matches {
for i := 0; i < len(match.Str); i++ {
if matched(i, match.MatchedIndexes) {
results.WriteString(styles.LogSearchHighlightStyle().Render(string(match.Str[i])))
} else {
results.WriteString(string(match.Str[i]))
}
}
results.WriteString("\n")
}

return results.String()
}

func matched(index int, matches []int) bool {
for _, i := range matches {
if index == i {
return true
}
}
return false
}

func wrapLogs(logs string, maxWidth int) string {
splitLogs := strings.Split(logs, "\n")
var logsBuilder strings.Builder
Expand Down
8 changes: 8 additions & 0 deletions pkg/charm/styles/styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,11 @@ func ContentStyle() lipgloss.Style {
func TableSelectedRowStyle() lipgloss.Style {
return lipgloss.NewStyle().Foreground(adaptColor)
}

func LogSearchHighlightStyle() lipgloss.Style {
return lipgloss.NewStyle().Foreground(adaptColor)
}

func LogSearchModeStyle() lipgloss.Style {
return lipgloss.NewStyle().Italic(true).Faint(true)
}
Loading

0 comments on commit a32effc

Please sign in to comment.