filter.go 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. package filter
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "os"
  7. "strings"
  8. "git.andreafazzi.eu/andrea/probo/pkg/store/file"
  9. "github.com/alecthomas/chroma/quick"
  10. "github.com/charmbracelet/bubbles/key"
  11. "github.com/charmbracelet/bubbles/spinner"
  12. tea "github.com/charmbracelet/bubbletea"
  13. "github.com/charmbracelet/lipgloss"
  14. "github.com/itchyny/gojq"
  15. foam "github.com/remogatto/sugarfoam"
  16. "github.com/remogatto/sugarfoam/components/group"
  17. "github.com/remogatto/sugarfoam/components/header"
  18. "github.com/remogatto/sugarfoam/components/help"
  19. "github.com/remogatto/sugarfoam/components/statusbar"
  20. "github.com/remogatto/sugarfoam/components/textinput"
  21. "github.com/remogatto/sugarfoam/components/viewport"
  22. "github.com/remogatto/sugarfoam/layout"
  23. )
  24. type FilterModel struct {
  25. // UI
  26. textInput *textinput.Model
  27. viewport *viewport.Model
  28. group *group.Model
  29. help *help.Model
  30. statusBar *statusbar.Model
  31. spinner spinner.Model
  32. // Layout
  33. document *layout.Layout
  34. // Key bindings
  35. bindings *keyBindings
  36. // file store
  37. store []any
  38. result []any
  39. // json
  40. lastQuery string
  41. FilteredJson string
  42. InputJson string
  43. Result string
  44. // filter file
  45. filterFilePath string
  46. state int
  47. filterType string
  48. }
  49. func New(path string, filterType string, stdin string) *FilterModel {
  50. textInput := textinput.New(
  51. textinput.WithPlaceholder("Write your jq filter here..."),
  52. )
  53. viewport := viewport.New()
  54. group := group.New(
  55. group.WithItems(textInput, viewport),
  56. group.WithLayout(
  57. layout.New(
  58. layout.WithStyles(&layout.Styles{Container: lipgloss.NewStyle().Padding(1, 0, 1, 0)}),
  59. layout.WithItem(textInput),
  60. layout.WithItem(viewport),
  61. ),
  62. ),
  63. )
  64. bindings := newBindings(group)
  65. statusBar := statusbar.New(bindings)
  66. s := spinner.New(
  67. spinner.WithStyle(
  68. lipgloss.NewStyle().Foreground(lipgloss.Color("265"))),
  69. )
  70. s.Spinner = spinner.Dot
  71. header := header.New(
  72. header.WithContent(
  73. lipgloss.NewStyle().
  74. Bold(true).
  75. Border(lipgloss.NormalBorder(), false, false, true, false).
  76. Render(filterTypeFormats[filterType]),
  77. ),
  78. )
  79. help := help.New(
  80. bindings,
  81. help.WithStyles(&foam.Styles{NoBorder: lipgloss.NewStyle().Padding(1, 1)}))
  82. document := layout.New(
  83. layout.WithStyles(&layout.Styles{Container: lipgloss.NewStyle().Margin(1)}),
  84. layout.WithItem(header),
  85. layout.WithItem(group),
  86. layout.WithItem(help),
  87. layout.WithItem(statusBar),
  88. )
  89. return &FilterModel{
  90. textInput: textInput,
  91. viewport: viewport,
  92. group: group,
  93. statusBar: statusBar,
  94. spinner: s,
  95. document: document,
  96. bindings: bindings,
  97. help: help,
  98. filterType: filterType,
  99. filterFilePath: path,
  100. InputJson: stdin,
  101. }
  102. }
  103. func (m *FilterModel) Init() tea.Cmd {
  104. var cmds []tea.Cmd
  105. cmds = append(cmds, m.group.Init(), m.loadStore(), m.spinner.Tick)
  106. m.group.Focus()
  107. return tea.Batch(cmds...)
  108. }
  109. func (m *FilterModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
  110. var cmds []tea.Cmd
  111. switch msg := msg.(type) {
  112. case tea.WindowSizeMsg:
  113. m.document.SetSize(msg.Width, msg.Height)
  114. case tea.KeyMsg:
  115. switch {
  116. case key.Matches(msg, m.bindings.quit):
  117. m.FilteredJson = ""
  118. return m, tea.Quit
  119. case key.Matches(msg, m.bindings.enter):
  120. m.marshalJSON()
  121. return m, tea.Quit
  122. }
  123. case storeLoadedMsg:
  124. cmds = append(cmds, m.handleStoreLoaded(msg))
  125. case resultMsg:
  126. cmds = append(cmds, m.handleFiltered(msg))
  127. m.state = FilterState
  128. case errorMsg:
  129. m.handleError(msg)
  130. m.state = ErrorState
  131. }
  132. cmds = m.handleState(msg, cmds)
  133. return m, tea.Batch(cmds...)
  134. }
  135. func (m *FilterModel) View() string {
  136. return m.document.View()
  137. }
  138. func (m *FilterModel) marshalJSON() {
  139. if m.FilteredJson == "" {
  140. return
  141. }
  142. if m.InputJson != "" {
  143. // result := make([]interface{}, 2)
  144. // err := json.Unmarshal([]byte(m.InputJson), &result[0])
  145. // if err != nil {
  146. // panic(err)
  147. // }
  148. // filtered := fmt.Sprintf("{\"%s\": %s}", m.filterType, m.FilteredJson)
  149. // err = json.Unmarshal([]byte(filtered), &result[1])
  150. // if err != nil {
  151. // panic(err)
  152. // }
  153. // resultJson, err := json.Marshal(result)
  154. // if err != nil {
  155. // panic(err)
  156. // }
  157. // m.Result = string(resultJson)
  158. m.Result = fmt.Sprintf("{%s, \"%s\": %s}", strings.Trim(m.InputJson, "{}"), m.filterType, m.FilteredJson)
  159. } else {
  160. var result interface{}
  161. filtered := fmt.Sprintf("{\"%s\": %s}", m.filterType, m.FilteredJson)
  162. err := json.Unmarshal([]byte(filtered), &result)
  163. if err != nil {
  164. panic(err)
  165. }
  166. resultJson, err := json.Marshal(result)
  167. if err != nil {
  168. panic(err)
  169. }
  170. m.Result = string(resultJson)
  171. }
  172. }
  173. func (m *FilterModel) showErrorOnStatusBar(err error) {
  174. m.statusBar.SetContent(
  175. stateFormats[ErrorState][0],
  176. fmt.Sprintf(stateFormats[ErrorState][1], err),
  177. stateFormats[ErrorState][2],
  178. )
  179. }
  180. func (m *FilterModel) handleError(msg tea.Msg) {
  181. err := msg.(errorMsg)
  182. m.statusBar.SetContent(
  183. stateFormats[ErrorState][0],
  184. fmt.Sprintf(stateFormats[ErrorState][1], err.error),
  185. stateFormats[ErrorState][2],
  186. )
  187. }
  188. func (m *FilterModel) handleStoreLoaded(msg tea.Msg) tea.Cmd {
  189. return func() tea.Msg {
  190. storeMsg := msg.(storeLoadedMsg)
  191. m.store = storeMsg.store
  192. m.state = FilterState
  193. if m.filterFilePath != "" {
  194. jq, err := os.ReadFile(m.filterFilePath)
  195. if err != nil {
  196. panic(err)
  197. }
  198. m.textInput.SetValue(strings.TrimSpace(string(jq)))
  199. return m.query(strings.TrimSpace(string(jq)))
  200. }
  201. coloredJson, err := toColoredJson(m.store)
  202. if err != nil {
  203. return errorMsg{err}
  204. }
  205. m.viewport.SetContent(coloredJson)
  206. return m.query(".")
  207. }
  208. }
  209. func (m *FilterModel) handleFiltered(msg tea.Msg) tea.Cmd {
  210. return func() tea.Msg {
  211. m.result = msg.(resultMsg).result
  212. coloredJson, err := toColoredJson(m.result)
  213. if err != nil {
  214. return errorMsg{err}
  215. }
  216. json, err := toJson(m.result)
  217. if err != nil {
  218. return errorMsg{err}
  219. }
  220. m.FilteredJson = json
  221. m.viewport.SetContent(coloredJson)
  222. return nil
  223. }
  224. }
  225. func (m *FilterModel) handleState(msg tea.Msg, cmds []tea.Cmd) []tea.Cmd {
  226. _, cmd := m.group.Update(msg)
  227. if m.state == LoadingStoreState {
  228. return m.updateSpinner(msg, cmd, cmds)
  229. }
  230. cmds = append(cmds, cmd, m.query(m.textInput.Value()))
  231. if m.state != ErrorState {
  232. m.statusBar.SetContent(stateFormats[FilterState]...)
  233. }
  234. return cmds
  235. }
  236. func (m *FilterModel) updateSpinner(msg tea.Msg, cmd tea.Cmd, cmds []tea.Cmd) []tea.Cmd {
  237. m.spinner, cmd = m.spinner.Update(msg)
  238. m.statusBar.SetContent(fmt.Sprintf(stateFormats[m.state][0], m.spinner.View()), stateFormats[m.state][1], stateFormats[m.state][2])
  239. cmds = append(cmds, cmd)
  240. return cmds
  241. }
  242. func (m *FilterModel) loadStore() tea.Cmd {
  243. return func() tea.Msg {
  244. var jsonStore []byte
  245. if m.filterType == "participants" {
  246. pStore, err := file.NewDefaultParticipantFileStore()
  247. if err != nil {
  248. return errorMsg{err}
  249. }
  250. jsonStore, err = pStore.Storer.Json()
  251. if err != nil {
  252. return errorMsg{err}
  253. }
  254. } else if m.filterType == "quizzes" {
  255. qStore, err := file.NewDefaultQuizFileStore()
  256. if err != nil {
  257. return errorMsg{err}
  258. }
  259. jsonStore, err = qStore.Storer.Json()
  260. if err != nil {
  261. return errorMsg{err}
  262. }
  263. } else {
  264. panic("Unknown filter type!")
  265. }
  266. v := make([]any, 0)
  267. err := json.Unmarshal(jsonStore, &v)
  268. if err != nil {
  269. return errorMsg{err}
  270. }
  271. return storeLoadedMsg{v}
  272. }
  273. }
  274. func (m *FilterModel) query(input string) tea.Cmd {
  275. return func() tea.Msg {
  276. if input == m.lastQuery {
  277. return nil
  278. }
  279. if m.state == LoadingStoreState {
  280. return nil
  281. }
  282. m.lastQuery = input
  283. query, err := gojq.Parse(input)
  284. if err != nil {
  285. return errorMsg{fmt.Errorf("jq query parse error: %v", err)}
  286. }
  287. var result []string
  288. iter := query.Run(m.store)
  289. for {
  290. v, ok := iter.Next()
  291. if !ok {
  292. break
  293. }
  294. if err, ok := v.(error); ok {
  295. return errorMsg{fmt.Errorf("jq query run error: %v", err)}
  296. }
  297. b, err := json.MarshalIndent(v, "", " ")
  298. if err != nil {
  299. return errorMsg{err}
  300. }
  301. result = append(result, string(b))
  302. }
  303. v := make([]any, 0)
  304. err = json.Unmarshal([]byte(strings.Join(result, "\n")), &v)
  305. if err != nil {
  306. return errorMsg{err}
  307. }
  308. return resultMsg{v}
  309. }
  310. }
  311. func toColoredJson(data []any) (string, error) {
  312. result, err := json.MarshalIndent(data, "", " ")
  313. if err != nil {
  314. return "", err
  315. }
  316. coloredBytes := make([]byte, 0)
  317. buffer := bytes.NewBuffer(coloredBytes)
  318. err = quick.Highlight(buffer, string(result), "json", "terminal16m", "dracula")
  319. if err != nil {
  320. panic(err)
  321. }
  322. return sanitize(buffer.String()), nil
  323. }
  324. func toJson(data []any) (string, error) {
  325. result, err := json.Marshal(data)
  326. if err != nil {
  327. return "", err
  328. }
  329. return string(result), nil
  330. }
  331. func sanitize(text string) string {
  332. // FIXME: The use of a standard '-' character causes rendering
  333. // issues within the viewport. Further investigation is
  334. // required to resolve this problem.
  335. return strings.Replace(text, "-", "–", -1)
  336. }