From 8d6872825149bfdaaf46ccfa71e9394a5e36d26f Mon Sep 17 00:00:00 2001 From: kevin olson Date: Fri, 30 Aug 2024 16:45:23 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20nice=20start,=20but=20we=20need?= =?UTF-8?q?=20to=20get=20more=20flexibility=20with=20loadPageFunc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/cmd/index/index.go | 95 ++++++++++++------------- pkg/ui/viewport.go | 157 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 195 insertions(+), 57 deletions(-) diff --git a/pkg/cmd/index/index.go b/pkg/cmd/index/index.go index 156b3e2..44ac81f 100644 --- a/pkg/cmd/index/index.go +++ b/pkg/cmd/index/index.go @@ -15,7 +15,6 @@ import ( ) func Command() *cobra.Command { - cmd := &cobra.Command{ Use: "index ", Short: i18n.C.IndexShort, @@ -34,31 +33,7 @@ func Command() *cobra.Command { Use: "list ", Short: i18n.C.IndexListShort, RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 1 { - return ui.Error(i18n.C.IndexErrorRequired) - } - - // Create a new IndexQueryParameters struct and set the values from the flags - queryParameters := sdk.IndexQueryParameters{} - for i := 0; i < keys.NumField(); i++ { - flag := utils.NormalizeString(keys.Field(i).Name) - if cmd.Flag(flag).Value.String() != "" { - field := reflect.ValueOf(&queryParameters).Elem().Field(i) - switch field.Kind() { - case reflect.String: - field.SetString(cmd.Flag(flag).Value.String()) - case reflect.Int: - intValue, err := strconv.Atoi(cmd.Flag(flag).Value.String()) - if err != nil { - fmt.Println(err) - continue - } - field.SetInt(int64(intValue)) - } - } - } - - response, err := session.Connect(config.Token()).GetIndex(args[0], queryParameters) + response, err := getIndexResponse(cmd, args) if err != nil { return err } @@ -71,35 +46,18 @@ func Command() *cobra.Command { Use: "browse ", Short: i18n.C.IndexBrowseShort, RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 1 { - return ui.Error(i18n.C.IndexErrorRequired) + response, err := getIndexResponse(cmd, args) + if err != nil { + return err } - // Create a new IndexQueryParameters struct and set the values from the flags - queryParameters := sdk.IndexQueryParameters{} - for i := 0; i < keys.NumField(); i++ { - flag := utils.NormalizeString(keys.Field(i).Name) - if cmd.Flag(flag).Value.String() != "" { - field := reflect.ValueOf(&queryParameters).Elem().Field(i) - switch field.Kind() { - case reflect.String: - field.SetString(cmd.Flag(flag).Value.String()) - case reflect.Int: - intValue, err := strconv.Atoi(cmd.Flag(flag).Value.String()) - if err != nil { - fmt.Println(err) - continue - } - field.SetInt(int64(intValue)) - } - } + loadPageFunc := func(index string, page int) (*sdk.IndexResponse, error) { + queryParams := buildQueryParameters(cmd) + queryParams.Page = page + return session.Connect(config.Token()).GetIndex(index, queryParams) } - response, err := session.Connect(config.Token()).GetIndex(args[0], queryParameters) - if err != nil { - return err - } - ui.Viewport(args[0], response.GetData()) + ui.ViewportPaginated(args[0], response.GetData(), response.Meta, loadPageFunc) return nil }, } @@ -109,3 +67,38 @@ func Command() *cobra.Command { return cmd } + +func getIndexResponse(cmd *cobra.Command, args []string) (*sdk.IndexResponse, error) { + if len(args) != 1 { + return nil, ui.Error(i18n.C.IndexErrorRequired) + } + + queryParameters := buildQueryParameters(cmd) + + return session.Connect(config.Token()).GetIndex(args[0], queryParameters) +} + +func buildQueryParameters(cmd *cobra.Command) sdk.IndexQueryParameters { + queryParameters := sdk.IndexQueryParameters{} + keys := reflect.TypeOf(sdk.IndexQueryParameters{}) + + for i := 0; i < keys.NumField(); i++ { + flag := utils.NormalizeString(keys.Field(i).Name) + if cmd.Flag(flag).Value.String() != "" { + field := reflect.ValueOf(&queryParameters).Elem().Field(i) + switch field.Kind() { + case reflect.String: + field.SetString(cmd.Flag(flag).Value.String()) + case reflect.Int: + intValue, err := strconv.Atoi(cmd.Flag(flag).Value.String()) + if err != nil { + fmt.Println(err) + continue + } + field.SetInt(int64(intValue)) + } + } + } + + return queryParameters +} diff --git a/pkg/ui/viewport.go b/pkg/ui/viewport.go index a05f006..41e782b 100644 --- a/pkg/ui/viewport.go +++ b/pkg/ui/viewport.go @@ -7,11 +7,14 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/vulncheck-oss/sdk" "strings" ) const useHighPerformanceRenderer = false +var subtle = lipgloss.AdaptiveColor{Light: "#D9DCCF", Dark: "#383838"} + var ( titleStyle = func() lipgloss.Style { b := lipgloss.RoundedBorder() @@ -27,12 +30,25 @@ var ( ) type model struct { - index string - content string - ready bool - viewport viewport.Model + index string + content string + ready bool + viewport viewport.Model + paginated bool + page int + totalPages int + showHelp bool + loadPage func(index string, page int) (*sdk.IndexResponse, error) +} + +type newPageMsg struct { + content string + page int + totalPages int } +type errMsg struct{ error } + func (m model) Init() tea.Cmd { return nil } @@ -45,10 +61,40 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: - if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" { + switch msg.String() { + case "ctrl+c", "q", "esc": + if m.showHelp { + m.showHelp = false + return m, nil + } return m, tea.Quit + case "?": + m.showHelp = !m.showHelp + return m, nil } + if m.paginated { + switch msg.String() { + case "left", "[": + if m.page > 1 { + m.page-- + return m, loadPage(m) + } + case "right", "]": + if m.page < m.totalPages { + m.page++ + return m, loadPage(m) + } + } + } + + case newPageMsg: + m.content = msg.content + m.page = msg.page + m.totalPages = msg.totalPages + m.viewport.SetContent(m.content) + return m, nil + case tea.WindowSizeMsg: headerHeight := lipgloss.Height(m.headerView()) footerHeight := lipgloss.Height(m.footerView()) @@ -92,10 +138,41 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Batch(cmds...) } +func loadPage(m model) tea.Cmd { + return func() tea.Msg { + response, err := m.loadPage(m.index, m.page) + if err != nil { + return errMsg{err} + } + + marshaled, err := json.MarshalIndent(response.GetData(), "", " ") + if err != nil { + return errMsg{err} + } + + var buf strings.Builder + err = quick.Highlight(&buf, string(marshaled), "json", "terminal256", "nord") + if err != nil { + return errMsg{err} + } + + return newPageMsg{ + content: buf.String(), + page: response.Meta.Page, + totalPages: response.Meta.TotalPages, + } + } +} + func (m model) View() string { if !m.ready { return "\n Initializing..." } + + if m.showHelp { + return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.helpView(), m.footerView()) + } + return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView()) } @@ -106,11 +183,47 @@ func (m model) headerView() string { } func (m model) footerView() string { - info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100)) + var info string + if m.paginated { + info = infoStyle.Render(fmt.Sprintf("%3.f%% | Page %d of %d | ? for help", m.viewport.ScrollPercent()*100, m.page, m.totalPages)) + } else { + info = infoStyle.Render(fmt.Sprintf("%3.f%% | ? for help", m.viewport.ScrollPercent()*100)) + } line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info))) return lipgloss.JoinHorizontal(lipgloss.Center, line, info) } +func (m model) helpView() string { + helpContent := ` +Hot Keys: + +q, esc : Close help / Quit +? : Toggle help +left, [ : Previous page +right, ] : Next page +` + + // Create a style for the help content + helpStyle := lipgloss.NewStyle(). + Width(42). // Increased width to accommodate border + Align(lipgloss.Left). + Border(lipgloss.RoundedBorder()). + BorderForeground(lipgloss.Color("240")). + Padding(1, 2) + + // Render the help content + renderedHelp := helpStyle.Render(helpContent) + + // Center the rendered help in the viewport + return lipgloss.Place( + m.viewport.Width, + m.viewport.Height, + lipgloss.Center, + lipgloss.Center, + renderedHelp, + ) +} + func max(a, b int) int { if a > b { return a @@ -143,3 +256,35 @@ func Viewport(index string, data interface{}) { fmt.Println("could not run program:", err) } } + +func ViewportPaginated(index string, initialData interface{}, initialMeta sdk.IndexMeta, loadPageFunc func(index string, page int) (*sdk.IndexResponse, error)) { + marshaled, err := json.MarshalIndent(initialData, "", " ") + if err != nil { + panic(err) + } + + var buf strings.Builder + err = quick.Highlight(&buf, string(marshaled), "json", "terminal256", "nord") + if err != nil { + panic(err) + } + + m := model{ + index: index, + content: buf.String(), + paginated: true, + page: initialMeta.Page, + totalPages: initialMeta.TotalPages, + loadPage: loadPageFunc, + } + + p := tea.NewProgram( + m, + tea.WithAltScreen(), + tea.WithMouseCellMotion(), + ) + + if _, err := p.Run(); err != nil { + fmt.Println("could not run program:", err) + } +}