filter.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. package filter
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "strings"
  6. "git.andreafazzi.eu/andrea/probo/pkg/store/file"
  7. "github.com/TylerBrock/colorjson"
  8. "github.com/charmbracelet/bubbles/help"
  9. "github.com/charmbracelet/bubbles/key"
  10. "github.com/charmbracelet/bubbles/spinner"
  11. tea "github.com/charmbracelet/bubbletea"
  12. "github.com/charmbracelet/lipgloss"
  13. "github.com/itchyny/gojq"
  14. "github.com/remogatto/sugarfoam/components/group"
  15. "github.com/remogatto/sugarfoam/components/header"
  16. "github.com/remogatto/sugarfoam/components/statusbar"
  17. "github.com/remogatto/sugarfoam/components/table"
  18. "github.com/remogatto/sugarfoam/components/textinput"
  19. "github.com/remogatto/sugarfoam/components/viewport"
  20. "github.com/remogatto/sugarfoam/layout"
  21. "github.com/remogatto/sugarfoam/layout/tiled"
  22. )
  23. type storeLoadedMsg struct {
  24. store []any
  25. }
  26. type resultMsg struct {
  27. result string
  28. }
  29. type errorMsg struct {
  30. error error
  31. }
  32. type FilterModel struct {
  33. // UI
  34. textInput *textinput.Model
  35. viewport *viewport.Model
  36. group *group.Model
  37. help help.Model
  38. statusBar *statusbar.Model
  39. spinner spinner.Model
  40. // Layout
  41. document *layout.Layout
  42. // Key bindings
  43. bindings *keyBindings
  44. // file store
  45. store []any
  46. // jq
  47. lastQuery string
  48. state int
  49. }
  50. type keyBindings struct {
  51. group *group.Model
  52. quit key.Binding
  53. }
  54. func (k *keyBindings) ShortHelp() []key.Binding {
  55. keys := make([]key.Binding, 0)
  56. current := k.group.Current()
  57. switch item := current.(type) {
  58. case *table.Model:
  59. keys = append(
  60. keys,
  61. item.KeyMap.LineUp,
  62. item.KeyMap.LineDown,
  63. )
  64. }
  65. keys = append(
  66. keys,
  67. k.quit,
  68. )
  69. return keys
  70. }
  71. func (k keyBindings) FullHelp() [][]key.Binding {
  72. return [][]key.Binding{
  73. {
  74. k.quit,
  75. },
  76. }
  77. }
  78. func newBindings(g *group.Model) *keyBindings {
  79. return &keyBindings{
  80. group: g,
  81. quit: key.NewBinding(
  82. key.WithKeys("esc"), key.WithHelp("esc", "Quit app"),
  83. ),
  84. }
  85. }
  86. func New() *FilterModel {
  87. textInput := textinput.New(
  88. textinput.WithPlaceholder("Write your jq filter here..."),
  89. )
  90. table := table.New()
  91. viewport := viewport.New()
  92. help := help.New()
  93. group := group.New(
  94. group.WithItems(textInput, viewport, table),
  95. group.WithLayout(
  96. layout.New(
  97. layout.WithStyles(&layout.Styles{Container: lipgloss.NewStyle().Padding(1, 0, 1, 0)}),
  98. layout.WithItem(textInput),
  99. layout.WithItem(tiled.New(viewport, table)),
  100. ),
  101. ),
  102. )
  103. bindings := newBindings(group)
  104. statusBar := statusbar.New(bindings)
  105. s := spinner.New(
  106. spinner.WithStyle(
  107. lipgloss.NewStyle().Foreground(lipgloss.Color("265"))),
  108. )
  109. s.Spinner = spinner.Dot
  110. header := header.New(
  111. header.WithContent(
  112. lipgloss.NewStyle().
  113. Bold(true).
  114. Border(lipgloss.NormalBorder(), false, false, true, false).
  115. Render("Participants filter 👫"),
  116. ),
  117. )
  118. document := layout.New(
  119. layout.WithStyles(&layout.Styles{Container: lipgloss.NewStyle().Margin(1)}),
  120. layout.WithItem(header),
  121. layout.WithItem(group),
  122. layout.WithItem(statusBar),
  123. )
  124. return &FilterModel{
  125. textInput: textInput,
  126. viewport: viewport,
  127. group: group,
  128. statusBar: statusBar,
  129. spinner: s,
  130. document: document,
  131. bindings: bindings,
  132. help: help,
  133. }
  134. }
  135. func (m *FilterModel) Init() tea.Cmd {
  136. var cmds []tea.Cmd
  137. cmds = append(cmds, m.group.Init(), m.loadStore(), m.spinner.Tick)
  138. m.group.Focus()
  139. return tea.Batch(cmds...)
  140. }
  141. func (m *FilterModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  142. var cmds []tea.Cmd
  143. switch msg := msg.(type) {
  144. case tea.WindowSizeMsg:
  145. m.document.SetSize(msg.Width, msg.Height)
  146. case tea.KeyMsg:
  147. switch {
  148. case key.Matches(msg, m.bindings.quit):
  149. return m, tea.Quit
  150. }
  151. case storeLoadedMsg:
  152. m.handleStoreLoaded(msg)
  153. case resultMsg:
  154. m.handleFiltered(msg)
  155. }
  156. cmds = m.handleState(msg, cmds)
  157. return m, tea.Batch(cmds...)
  158. }
  159. func (m *FilterModel) View() string {
  160. return m.document.View()
  161. }
  162. func (m *FilterModel) handleStoreLoaded(msg tea.Msg) {
  163. storeMsg := msg.(storeLoadedMsg)
  164. m.store = storeMsg.store
  165. m.state = FilterState
  166. jsonStore, err := m.storeToJson()
  167. if err != nil {
  168. panic(err)
  169. }
  170. m.viewport.SetContent(jsonStore)
  171. }
  172. func (m *FilterModel) handleFiltered(msg tea.Msg) {
  173. m.viewport.SetContent(sanitize(msg.(resultMsg).result))
  174. }
  175. func (m *FilterModel) handleState(msg tea.Msg, cmds []tea.Cmd) []tea.Cmd {
  176. _, cmd := m.group.Update(msg)
  177. if m.state == LoadingStoreState {
  178. return m.updateSpinner(msg, cmd, cmds)
  179. }
  180. cmds = append(cmds, cmd, m.query(m.textInput.Value()))
  181. m.statusBar.SetContent(formats[FilterState]...)
  182. return cmds
  183. }
  184. func (m *FilterModel) updateSpinner(msg tea.Msg, cmd tea.Cmd, cmds []tea.Cmd) []tea.Cmd {
  185. m.spinner, cmd = m.spinner.Update(msg)
  186. m.statusBar.SetContent(fmt.Sprintf(formats[m.state][0], m.spinner.View()), formats[m.state][1], formats[m.state][2])
  187. cmds = append(cmds, cmd)
  188. return cmds
  189. }
  190. func (m *FilterModel) loadStore() tea.Cmd {
  191. return func() tea.Msg {
  192. pStore, err := file.NewDefaultParticipantFileStore()
  193. if err != nil {
  194. return errorMsg{err}
  195. }
  196. jsonStore, err := pStore.Storer.Json()
  197. if err != nil {
  198. return errorMsg{err}
  199. }
  200. v := make([]any, 0)
  201. err = json.Unmarshal(jsonStore, &v)
  202. if err != nil {
  203. return errorMsg{err}
  204. }
  205. return storeLoadedMsg{v}
  206. }
  207. }
  208. func (m *FilterModel) query(input string) tea.Cmd {
  209. return func() tea.Msg {
  210. if input == m.lastQuery {
  211. return nil
  212. }
  213. if m.state == LoadingStoreState {
  214. return nil
  215. }
  216. m.lastQuery = input
  217. query, err := gojq.Parse(input)
  218. if err != nil {
  219. return errorMsg{err}
  220. }
  221. var result []string
  222. iter := query.Run(m.store)
  223. for {
  224. v, ok := iter.Next()
  225. if !ok {
  226. break
  227. }
  228. if err, ok := v.(error); ok {
  229. return errorMsg{err}
  230. }
  231. f := colorjson.NewFormatter()
  232. f.Indent = 2
  233. b, err := f.Marshal(v)
  234. if err != nil {
  235. return errorMsg{err}
  236. }
  237. result = append(result, string(b))
  238. }
  239. return resultMsg{strings.Join(result, "\n")}
  240. }
  241. }
  242. func (m *FilterModel) storeToJson() (string, error) {
  243. f := colorjson.NewFormatter()
  244. f.Indent = 2
  245. result, err := f.Marshal(m.store)
  246. if err != nil {
  247. return "", err
  248. }
  249. return sanitize(string(result)), nil
  250. }
  251. func sanitize(text string) string {
  252. // FIXME: The use of a standard '-' character causes rendering
  253. // issues within the viewport. Further investigation is
  254. // required to resolve this problem.
  255. return strings.Replace(text, "-", "–", -1)
  256. }