probo/cmd/filter/filter.go

328 lines
6.2 KiB
Go
Raw Normal View History

2024-03-25 15:53:20 +01:00
package filter
import (
2024-03-27 11:45:33 +01:00
"encoding/json"
2024-03-25 15:53:20 +01:00
"fmt"
2024-03-27 11:45:33 +01:00
"strings"
2024-03-25 15:53:20 +01:00
"git.andreafazzi.eu/andrea/probo/pkg/store/file"
2024-03-27 11:45:33 +01:00
"github.com/TylerBrock/colorjson"
2024-03-25 15:53:20 +01:00
"github.com/charmbracelet/bubbles/help"
"github.com/charmbracelet/bubbles/key"
2024-03-27 11:45:33 +01:00
"github.com/charmbracelet/bubbles/spinner"
2024-03-25 15:53:20 +01:00
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
2024-03-27 11:45:33 +01:00
"github.com/itchyny/gojq"
2024-03-25 15:53:20 +01:00
"github.com/remogatto/sugarfoam/components/group"
"github.com/remogatto/sugarfoam/components/header"
"github.com/remogatto/sugarfoam/components/statusbar"
"github.com/remogatto/sugarfoam/components/table"
"github.com/remogatto/sugarfoam/components/textinput"
"github.com/remogatto/sugarfoam/components/viewport"
"github.com/remogatto/sugarfoam/layout"
"github.com/remogatto/sugarfoam/layout/tiled"
)
2024-03-27 11:45:33 +01:00
type storeLoadedMsg struct {
store []any
2024-03-25 15:53:20 +01:00
}
2024-03-27 11:45:33 +01:00
type resultMsg struct {
result string
}
type errorMsg struct {
error error
2024-03-25 15:53:20 +01:00
}
type FilterModel struct {
// UI
2024-03-27 11:45:33 +01:00
textInput *textinput.Model
viewport *viewport.Model
2024-03-25 15:53:20 +01:00
group *group.Model
help help.Model
statusBar *statusbar.Model
2024-03-27 11:45:33 +01:00
spinner spinner.Model
2024-03-25 15:53:20 +01:00
// Layout
document *layout.Layout
// Key bindings
bindings *keyBindings
2024-03-27 11:45:33 +01:00
// file store
store []any
// jq
lastQuery string
2024-03-25 15:53:20 +01:00
state int
}
type keyBindings struct {
group *group.Model
quit key.Binding
}
func (k *keyBindings) ShortHelp() []key.Binding {
keys := make([]key.Binding, 0)
current := k.group.Current()
switch item := current.(type) {
case *table.Model:
keys = append(
keys,
item.KeyMap.LineUp,
item.KeyMap.LineDown,
)
}
keys = append(
keys,
k.quit,
)
return keys
}
func (k keyBindings) FullHelp() [][]key.Binding {
return [][]key.Binding{
{
k.quit,
},
}
}
func newBindings(g *group.Model) *keyBindings {
return &keyBindings{
group: g,
quit: key.NewBinding(
key.WithKeys("esc"), key.WithHelp("esc", "Quit app"),
),
}
}
func New() *FilterModel {
2024-03-27 11:45:33 +01:00
textInput := textinput.New(
2024-03-25 15:53:20 +01:00
textinput.WithPlaceholder("Write your jq filter here..."),
)
table := table.New()
viewport := viewport.New()
2024-03-27 11:45:33 +01:00
2024-03-25 15:53:20 +01:00
help := help.New()
group := group.New(
2024-03-27 11:45:33 +01:00
group.WithItems(textInput, viewport, table),
2024-03-25 15:53:20 +01:00
group.WithLayout(
layout.New(
layout.WithStyles(&layout.Styles{Container: lipgloss.NewStyle().Padding(1, 0, 1, 0)}),
2024-03-27 11:45:33 +01:00
layout.WithItem(textInput),
2024-03-25 15:53:20 +01:00
layout.WithItem(tiled.New(viewport, table)),
),
),
)
bindings := newBindings(group)
statusBar := statusbar.New(bindings)
2024-03-27 11:45:33 +01:00
s := spinner.New(
spinner.WithStyle(
lipgloss.NewStyle().Foreground(lipgloss.Color("265"))),
)
s.Spinner = spinner.Dot
2024-03-25 15:53:20 +01:00
header := header.New(
header.WithContent(
lipgloss.NewStyle().
Bold(true).
Border(lipgloss.NormalBorder(), false, false, true, false).
Render("Participants filter 👫"),
),
)
document := layout.New(
layout.WithStyles(&layout.Styles{Container: lipgloss.NewStyle().Margin(1)}),
layout.WithItem(header),
layout.WithItem(group),
layout.WithItem(statusBar),
)
return &FilterModel{
2024-03-27 11:45:33 +01:00
textInput: textInput,
viewport: viewport,
2024-03-25 15:53:20 +01:00
group: group,
statusBar: statusBar,
2024-03-27 11:45:33 +01:00
spinner: s,
2024-03-25 15:53:20 +01:00
document: document,
bindings: bindings,
help: help,
}
}
func (m *FilterModel) Init() tea.Cmd {
var cmds []tea.Cmd
2024-03-27 11:45:33 +01:00
cmds = append(cmds, m.group.Init(), m.loadStore(), m.spinner.Tick)
2024-03-25 15:53:20 +01:00
m.group.Focus()
return tea.Batch(cmds...)
}
func (m *FilterModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.document.SetSize(msg.Width, msg.Height)
case tea.KeyMsg:
switch {
case key.Matches(msg, m.bindings.quit):
return m, tea.Quit
}
case storeLoadedMsg:
m.handleStoreLoaded(msg)
2024-03-27 11:45:33 +01:00
case resultMsg:
m.handleFiltered(msg)
2024-03-25 15:53:20 +01:00
}
cmds = m.handleState(msg, cmds)
return m, tea.Batch(cmds...)
}
func (m *FilterModel) View() string {
return m.document.View()
}
func (m *FilterModel) handleStoreLoaded(msg tea.Msg) {
storeMsg := msg.(storeLoadedMsg)
m.store = storeMsg.store
m.state = FilterState
2024-03-27 11:45:33 +01:00
jsonStore, err := m.storeToJson()
if err != nil {
panic(err)
}
m.viewport.SetContent(jsonStore)
}
func (m *FilterModel) handleFiltered(msg tea.Msg) {
m.viewport.SetContent(sanitize(msg.(resultMsg).result))
2024-03-25 15:53:20 +01:00
}
func (m *FilterModel) handleState(msg tea.Msg, cmds []tea.Cmd) []tea.Cmd {
_, cmd := m.group.Update(msg)
2024-03-27 11:45:33 +01:00
if m.state == LoadingStoreState {
return m.updateSpinner(msg, cmd, cmds)
}
2024-03-25 15:53:20 +01:00
2024-03-27 11:45:33 +01:00
cmds = append(cmds, cmd, m.query(m.textInput.Value()))
m.statusBar.SetContent(formats[FilterState]...)
return cmds
}
func (m *FilterModel) updateSpinner(msg tea.Msg, cmd tea.Cmd, cmds []tea.Cmd) []tea.Cmd {
m.spinner, cmd = m.spinner.Update(msg)
m.statusBar.SetContent(fmt.Sprintf(formats[m.state][0], m.spinner.View()), formats[m.state][1], formats[m.state][2])
cmds = append(cmds, cmd)
2024-03-25 15:53:20 +01:00
return cmds
}
2024-03-27 11:45:33 +01:00
func (m *FilterModel) loadStore() tea.Cmd {
2024-03-25 15:53:20 +01:00
return func() tea.Msg {
pStore, err := file.NewDefaultParticipantFileStore()
if err != nil {
2024-03-27 11:45:33 +01:00
return errorMsg{err}
2024-03-25 15:53:20 +01:00
}
2024-03-27 11:45:33 +01:00
jsonStore, err := pStore.Storer.Json()
if err != nil {
return errorMsg{err}
}
v := make([]any, 0)
err = json.Unmarshal(jsonStore, &v)
if err != nil {
return errorMsg{err}
}
return storeLoadedMsg{v}
2024-03-25 15:53:20 +01:00
}
}
2024-03-27 11:45:33 +01:00
func (m *FilterModel) query(input string) tea.Cmd {
return func() tea.Msg {
if input == m.lastQuery {
return nil
}
if m.state == LoadingStoreState {
return nil
}
m.lastQuery = input
query, err := gojq.Parse(input)
if err != nil {
return errorMsg{err}
}
var result []string
iter := query.Run(m.store)
for {
v, ok := iter.Next()
if !ok {
break
}
if err, ok := v.(error); ok {
return errorMsg{err}
}
f := colorjson.NewFormatter()
f.Indent = 2
b, err := f.Marshal(v)
if err != nil {
return errorMsg{err}
}
result = append(result, string(b))
}
return resultMsg{strings.Join(result, "\n")}
}
}
func (m *FilterModel) storeToJson() (string, error) {
f := colorjson.NewFormatter()
f.Indent = 2
result, err := f.Marshal(m.store)
if err != nil {
return "", err
}
return sanitize(string(result)), nil
}
func sanitize(text string) string {
// FIXME: The use of a standard '-' character causes rendering
// issues within the viewport. Further investigation is
// required to resolve this problem.
return strings.Replace(text, "-", "", -1)
}