From 159969fe3ea17a84c20f1b901aacf2b6e84b20f9 Mon Sep 17 00:00:00 2001 From: k1nho Date: Tue, 15 Aug 2023 16:17:05 -0400 Subject: [PATCH 1/8] create pizza show testing go-client Adding general repository info to TUI (issues, forks, size, stars) centralized state manager structure with nested models, and start of contributors model Added contributor model stats and interactivity through table selection in dashboard model tidy improving error handling flow, and adding basic dashboard responsiveness for horizontal layouts tidy --- cmd/root/root.go | 3 + cmd/show/contributors.go | 190 +++++++++++++++++++++ cmd/show/dashboard.go | 360 +++++++++++++++++++++++++++++++++++++++ cmd/show/show.go | 85 +++++++++ cmd/show/styles.go | 42 +++++ cmd/show/tui.go | 85 +++++++++ go.mod | 2 + go.sum | 37 ++++ pkg/api/constants.go | 6 + 9 files changed, 810 insertions(+) create mode 100644 cmd/show/contributors.go create mode 100644 cmd/show/dashboard.go create mode 100644 cmd/show/show.go create mode 100644 cmd/show/styles.go create mode 100644 cmd/show/tui.go create mode 100644 pkg/api/constants.go diff --git a/cmd/root/root.go b/cmd/root/root.go index af1a376..79166ea 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -11,6 +11,8 @@ import ( "github.com/open-sauced/pizza-cli/cmd/version" "github.com/open-sauced/pizza-cli/pkg/constants" "github.com/spf13/cobra" + "github.com/open-sauced/pizza-cli/cmd/show" + "github.com/open-sauced/pizza-cli/pkg/api" ) // NewRootCommand bootstraps a new root cobra command for the pizza CLI @@ -41,6 +43,7 @@ func NewRootCommand() (*cobra.Command, error) { cmd.AddCommand(auth.NewLoginCommand()) cmd.AddCommand(insights.NewInsightsCommand()) cmd.AddCommand(version.NewVersionCommand()) + cmd.AddCommand(show.NewShowCommand()) return cmd, nil } diff --git a/cmd/show/contributors.go b/cmd/show/contributors.go new file mode 100644 index 0000000..3e04ce2 --- /dev/null +++ b/cmd/show/contributors.go @@ -0,0 +1,190 @@ +package show + +import ( + "context" + "errors" + "fmt" + "sync" + + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + client "github.com/open-sauced/go-api/client" +) + +// ContributorModel holds all the information related to a contributor +type ContributorModel struct { + username string + userInfo *client.DbUser + userPrs []client.DbPullRequest +} + +type ( + // BackMsg: message to signal main model that we are back to dashboard when backspace is pressed + BackMsg struct{} + + // ContributorErrMsg: message to signal that an error occurred when fetching contributor information + ContributorErrMsg struct { + name string + err error + } +) + +// InitContributor: initializes the contributorModel +func InitContributor(contributorName string) (ContributorModel, error) { + var contributorModel ContributorModel + contributorModel.username = contributorName + + err := contributorModel.fetchUser() + if err != nil { + return contributorModel, err + } + + return contributorModel, nil +} + +func (m ContributorModel) Init() tea.Cmd { return nil } + +func (m ContributorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "backspace": + return m, func() tea.Msg { return BackMsg{} } + case "q", "esc", "ctrl+c", "ctrl+d": + return m, tea.Quit + } + } + return m, cmd +} + +func (m ContributorModel) View() string { + return m.drawContributorView() +} + +// fetchUser: fetches all the user information (general info, and pull requests) +func (m *ContributorModel) fetchUser() error { + var ( + wg sync.WaitGroup + errChan = make(chan error, 2) + ) + + wg.Add(2) + go func() { + defer wg.Done() + userInfo, err := fetchContributorInfo(m.username) + if err != nil { + errChan <- err + return + } + m.userInfo = userInfo + + }() + + go func() { + defer wg.Done() + userPRs, err := fetchContributorPRs(m.username) + if err != nil { + errChan <- err + return + } + m.userPrs = userPRs + }() + + wg.Wait() + close(errChan) + if len(errChan) > 0 { + var allErrors error + for err := range errChan { + allErrors = errors.Join(allErrors, err) + } + return allErrors + } + + return nil +} + +// fetchContributorInfo: fetches the contributor info +func fetchContributorInfo(name string) (*client.DbUser, error) { + config := client.NewConfiguration() + apiClient := client.NewAPIClient(config) + ctx := context.WithValue(context.Background(), client.ContextServerIndex, 1) + + resp, r, err := apiClient.UserServiceAPI.FindOneUserByUserame(ctx, name).Execute() + if err != nil { + return nil, err + } + + if r.StatusCode != 200 { + return nil, fmt.Errorf("HTTP failed: %d", r.StatusCode) + } + + return resp, nil +} + +// fetchContributorPRs: fetches the contributor pull requests +func fetchContributorPRs(name string) ([]client.DbPullRequest, error) { + config := client.NewConfiguration() + apiClient := client.NewAPIClient(config) + ctx := context.WithValue(context.Background(), client.ContextServerIndex, 1) + + resp, r, err := apiClient.UserServiceAPI.FindContributorPullRequests(ctx, name).Execute() + if err != nil { + return nil, err + } + + if r.StatusCode != 200 { + return nil, fmt.Errorf("HTTP failed: %d", r.StatusCode) + } + + return resp.Data, nil +} + +// drawContributorView: view of the contributor model +func (m *ContributorModel) drawContributorView() string { + return Viewport.Copy().Render(lipgloss.JoinVertical(lipgloss.Center, m.drawContributorInfo(), m.drawPullRequests())) +} + +// drawContributorInfo: view of the contributor info (open issues, pr velocity, pr count, maintainer) +func (m *ContributorModel) drawContributorInfo() string { + userOpenIssues := fmt.Sprintf("📄 Issues: %d", m.userInfo.OpenIssues) + isUserMaintainer := fmt.Sprintf("🔨 Maintainer: %t", m.userInfo.GetIsMaintainer()) + prVelocity := fmt.Sprintf("🔥 PR Velocity (30d): %d%%", m.userInfo.RecentPullRequestVelocityCount) + prCount := fmt.Sprintf("🚀 PR Count (30d): %d", m.userInfo.RecentPullRequestsCount) + + prStats := lipgloss.JoinVertical(lipgloss.Left, TextContainer.Render(prVelocity), TextContainer.Render(prCount)) + issuesAndMaintainer := lipgloss.JoinVertical(lipgloss.Center, TextContainer.Render(userOpenIssues), TextContainer.Render(isUserMaintainer)) + + contributorInfo := lipgloss.JoinHorizontal(lipgloss.Center, prStats, issuesAndMaintainer) + contributorView := lipgloss.JoinVertical(lipgloss.Center, m.userInfo.Login, contributorInfo) + + return SquareBorder.Render(contributorView) +} + +// drawPullRequests: view of the contributor pull requests (draws the last 5 pull requests) +func (m *ContributorModel) drawPullRequests() string { + if len(m.userPrs) == 0 { + return "" + } + + pullRequests := []string{} + var numberOfPrs int + + if len(m.userPrs) > 5 { + numberOfPrs = 5 + } else { + numberOfPrs = len(m.userPrs) + } + + for i := 0; i < numberOfPrs; i++ { + prContainer := TextContainer.Render(fmt.Sprintf("#%d %s\n%s\n(%s)", m.userPrs[i].Number, m.userPrs[i].GetFullName(), + m.userPrs[i].Title, m.userPrs[i].State)) + pullRequests = append(pullRequests, prContainer) + } + + formattedPrs := lipgloss.JoinVertical(lipgloss.Left, pullRequests...) + title := lipgloss.NewStyle().AlignHorizontal(lipgloss.Center).Render("✨ Latest Pull Requests") + + pullRequestView := lipgloss.JoinVertical(lipgloss.Center, title, formattedPrs) + return WidgetContainer.Render(pullRequestView) +} diff --git a/cmd/show/dashboard.go b/cmd/show/dashboard.go new file mode 100644 index 0000000..e6a056f --- /dev/null +++ b/cmd/show/dashboard.go @@ -0,0 +1,360 @@ +package show + +import ( + "context" + "errors" + "fmt" + "strconv" + "strings" + "sync" + + "github.com/charmbracelet/bubbles/table" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + client "github.com/open-sauced/go-api/client" +) + +const ( + newContributorsView = iota + alumniContributorsView +) + +// DashboardModel holds all the information related to the repository queried (issues, stars, new contributors, alumni contributors) +type DashboardModel struct { + newContributorsTable table.Model + alumniContributorsTable table.Model + RepositoryInfo *client.DbRepo + contributorErr string + tableView int + queryOptions [3]int + APIClient *client.APIClient + serverContext context.Context +} + +// SelectMsg: message to signal the main model that we want to go to the contributor model when 'enter' is pressed +type SelectMsg struct { + contributorName string +} + +// FetchRepoInfo: initializes the dashboard model +func InitDashboard(opts *Options) (DashboardModel, error) { + model := DashboardModel{} + err := validateShowQuery(opts) + if err != nil { + return model, err + } + ownerRepo := strings.Split(opts.RepoName, "/") + + resp, r, err := opts.APIClient.RepositoryServiceAPI.FindOneByOwnerAndRepo(opts.ServerContext, ownerRepo[0], ownerRepo[1]).Execute() + if err != nil { + return model, err + } + + if r.StatusCode != 200 { + return model, fmt.Errorf("HTTP status: %d", r.StatusCode) + } + + // configuring the dashboardModel + model.RepositoryInfo = resp + model.queryOptions = [3]int{opts.Page, opts.Limit, opts.Range} + model.APIClient = opts.APIClient + model.serverContext = opts.ServerContext + + // Fetching all contributors + err = model.FetchAllContributors() + if err != nil { + return model, err + } + + return model, nil +} + +func (m DashboardModel) Init() tea.Cmd { return nil } + +func (m DashboardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + switch msg := msg.(type) { + case ContributorErrMsg: + m.contributorErr = fmt.Sprintf("🚧 could not fetch %s: %s", msg.name, msg.err.Error()) + default: + m.contributorErr = "" + + case tea.WindowSizeMsg: + WindowSize = msg + width, _ := m.alumniContributorsTable.Width(), m.alumniContributorsTable.Height() + m.alumniContributorsTable.SetWidth(max(msg.Width-width, 5)) + m.newContributorsTable.SetWidth(max(msg.Width-width, 5)) + + case tea.KeyMsg: + switch msg.String() { + case "right", "l": + m.tableView = (m.tableView + 1) % 2 + case "left", "h": + if m.tableView-1 <= 0 { + m.tableView = 0 + } else { + m.tableView-- + } + case "q", "esc", "ctrl+c", "ctrl+d": + return m, tea.Quit + case "enter": + var contributorName string + switch m.tableView { + case newContributorsView: + if len(m.newContributorsTable.Rows()) > 0 { + contributorName = m.newContributorsTable.SelectedRow()[1] + return m, func() tea.Msg { return SelectMsg{contributorName} } + } + case alumniContributorsView: + if len(m.alumniContributorsTable.Rows()) > 0 { + contributorName = m.alumniContributorsTable.SelectedRow()[1] + return m, func() tea.Msg { return SelectMsg{contributorName} } + } + } + } + + switch m.tableView { + case newContributorsView: + m.newContributorsTable, cmd = m.newContributorsTable.Update(msg) + case alumniContributorsView: + m.alumniContributorsTable, cmd = m.alumniContributorsTable.Update(msg) + } + } + + return m, cmd +} + +func (m DashboardModel) View() string { + return m.drawDashboardView() +} + +// drawTitle: view of PIZZA +func (m *DashboardModel) drawTitle() string { + titleRunes1 := []rune{'█', '█', '█', '█', '█', '█', '╗', ' ', '█', '█', '╗', '█', '█', '█', '█', '█', '█', '█', '╗', '█', '█', '█', '█', '█', '█', '█', '╗', ' ', '█', '█', '█', '█', '█', '╗', ' '} + titleRunes2 := []rune{'█', '█', '╔', '═', '═', '█', '█', '╗', '█', '█', '║', '╚', '═', '═', '█', '█', '█', '╔', '╝', '╚', '═', '═', '█', '█', '█', '╔', '╝', '█', '█', '╔', '═', '═', '█', '█', '╗'} + titleRunes3 := []rune{'█', '█', '█', '█', '█', '█', '╔', '╝', '█', '█', '║', ' ', ' ', '█', '█', '█', '╔', '╝', ' ', ' ', ' ', '█', '█', '█', '╔', '╝', ' ', '█', '█', '█', '█', '█', '█', '█', '║'} + titleRunes4 := []rune{'█', '█', '╔', '═', '═', '═', '╝', ' ', '█', '█', '║', ' ', '█', '█', '█', '╔', '╝', ' ', ' ', ' ', '█', '█', '█', '╔', '╝', ' ', ' ', '█', '█', '╔', '═', '═', '█', '█', '║'} + titleRunes5 := []rune{'█', '█', '║', ' ', ' ', ' ', ' ', ' ', '█', '█', '║', '█', '█', '█', '█', '█', '█', '█', '╗', '█', '█', '█', '█', '█', '█', '█', '╗', '█', '█', '║', ' ', ' ', '█', '█', '║'} + titleRunes6 := []rune{'╚', '═', '╝', ' ', ' ', ' ', ' ', ' ', '╚', '═', '╝', '╚', '═', '═', '═', '═', '═', '═', '╝', '╚', '═', '═', '═', '═', '═', '═', '╝', '╚', '═', '╝', ' ', ' ', '╚', '═', '╝'} + + title1 := lipgloss.JoinHorizontal(lipgloss.Left, string(titleRunes1)) + title2 := lipgloss.JoinHorizontal(lipgloss.Left, string(titleRunes2)) + title3 := lipgloss.JoinHorizontal(lipgloss.Left, string(titleRunes3)) + title4 := lipgloss.JoinHorizontal(lipgloss.Left, string(titleRunes4)) + title5 := lipgloss.JoinHorizontal(lipgloss.Left, string(titleRunes5)) + title6 := lipgloss.JoinHorizontal(lipgloss.Left, string(titleRunes6)) + title := lipgloss.JoinVertical(lipgloss.Center, title1, title2, title3, title4, title5, title6) + titleView := lipgloss.NewStyle().Foreground(Color).Render(title) + + return titleView +} + +// drawRepositoryInfo: view of the repository info (name, stars, size, and issues) +func (m *DashboardModel) drawRepositoryInfo() string { + repoName := lipgloss.NewStyle().Bold(true).AlignHorizontal(lipgloss.Center).Render(fmt.Sprintf("Repository: %s", m.RepositoryInfo.FullName)) + repoStars := fmt.Sprintf("🌟 stars: %d", m.RepositoryInfo.Stars) + repoSize := fmt.Sprintf("💾 size: %dB", m.RepositoryInfo.Size) + repoIssues := fmt.Sprintf("📄 issues: %d", m.RepositoryInfo.Issues) + repoForks := fmt.Sprintf("⑂ forks: %d", m.RepositoryInfo.Forks) + + issuesAndForks := lipgloss.JoinVertical(lipgloss.Center, TextContainer.Render(repoIssues), TextContainer.Render(repoForks)) + sizeAndStars := lipgloss.JoinVertical(lipgloss.Left, TextContainer.Render(repoSize), TextContainer.Render(repoStars)) + + repoGeneralSection := lipgloss.JoinHorizontal(lipgloss.Center, issuesAndForks, sizeAndStars) + repositoryInfoView := lipgloss.JoinVertical(lipgloss.Center, repoName, repoGeneralSection) + frame := SquareBorder.Render(repositoryInfoView) + + return frame +} + +// drawMetrics: view of metrics includes. +// - new contributors table +// - alumni contributors table +func (m *DashboardModel) drawMetrics() string { + var newContributorsDisplay, alumniContributorsDisplay string + + switch m.tableView { + case newContributorsView: + newContributorsDisplay = lipgloss.JoinVertical(lipgloss.Center, TableTitle.Render("🍕 New Contributors"), ActiveStyle.Render(m.newContributorsTable.View())) + alumniContributorsDisplay = lipgloss.JoinVertical(lipgloss.Center, TableTitle.Render("🍁 Alumni Contributors"), InactiveStyle.Render(m.alumniContributorsTable.View())) + case alumniContributorsView: + newContributorsDisplay = lipgloss.JoinVertical(lipgloss.Center, TableTitle.Render("🍕 New Contributors"), InactiveStyle.Render(m.newContributorsTable.View())) + alumniContributorsDisplay = lipgloss.JoinVertical(lipgloss.Center, TableTitle.Render("🍁 Alumni Contributors"), ActiveStyle.Render(m.alumniContributorsTable.View())) + } + + contributorsMetrics := lipgloss.JoinHorizontal(lipgloss.Center, WidgetContainer.Render(newContributorsDisplay), WidgetContainer.Render(alumniContributorsDisplay)) + + return contributorsMetrics +} + +// drawDashboardView: this is the main model view (shows repository info and tables) +func (m *DashboardModel) drawDashboardView() string { + if WindowSize.Width == 0 { + return "Loading..." + } + var wg sync.WaitGroup + + wg.Add(3) + var titleView, repoInfoView, metricsView string + go func() { + defer wg.Done() + titleView = m.drawTitle() + }() + + go func() { + defer wg.Done() + repoInfoView = m.drawRepositoryInfo() + }() + + go func() { + defer wg.Done() + metricsView = m.drawMetrics() + }() + wg.Wait() + + mainView := lipgloss.JoinVertical(lipgloss.Center, titleView, repoInfoView, metricsView, m.contributorErr) + _, h := lipgloss.Size(mainView) + if WindowSize.Height < h { + contentLeft := lipgloss.JoinVertical(lipgloss.Center, titleView, repoInfoView) + mainView = lipgloss.JoinHorizontal(lipgloss.Center, contentLeft, metricsView) + } + frame := Viewport.Render(mainView) + return frame +} + +// validateShowQuery: validates fields set to query the contributor tables +func validateShowQuery(opts *Options) error { + if opts.RepoName == "" { + return errors.New("no repository was provided") + } + + if len(strings.Split(opts.RepoName, "/")) != 2 { + return errors.New("wrong repository name format. Must be in the form owner/name, i.e(open-sauced/pizza)") + } + if opts.Limit < 1 { + return errors.New("--limit flag must be a positive integer value") + } + if opts.Range < 1 { + return errors.New("--range flag must be a positive integer value") + } + + if opts.Page < 1 { + return errors.New("--page flag must be a positive integer value") + } + + return nil +} + +// FetchAllContributors: fetchs and sets all the contributors (new, alumni) +func (m *DashboardModel) FetchAllContributors() error { + var ( + errorChan = make(chan error, 2) + wg sync.WaitGroup + ) + + wg.Add(2) + go func() { + defer wg.Done() + newContributors, err := m.FetchNewContributors() + if err != nil { + errorChan <- err + return + } + m.newContributorsTable = setupContributorsTable(newContributors) + }() + + go func() { + defer wg.Done() + alumniContributors, err := m.FetchAlumniContributors() + if err != nil { + errorChan <- err + return + } + m.alumniContributorsTable = setupContributorsTable(alumniContributors) + }() + + wg.Wait() + close(errorChan) + if len(errorChan) > 0 { + var allErrors error + for err := range errorChan { + allErrors = errors.Join(allErrors, err) + } + return allErrors + } + + return nil +} + +// FetchNewContributors: Returns all the new contributors +func (m *DashboardModel) FetchNewContributors() ([]client.DbPullRequestContributor, error) { + resp, r, err := m.APIClient.ContributorsServiceAPI.NewPullRequestContributors(m.serverContext).Page(int32(m.queryOptions[0])). + Limit(int32(m.queryOptions[1])).RepoIds(strconv.Itoa(int(m.RepositoryInfo.Id))).Execute() + if err != nil { + return nil, err + } + + if r.StatusCode != 200 { + return nil, fmt.Errorf("HTTP status: %d", r.StatusCode) + } + + return resp.Data, nil + +} + +// FetchAlumniContributors: Returns all alumni contributors +func (m *DashboardModel) FetchAlumniContributors() ([]client.DbPullRequestContributor, error) { + resp, r, err := m.APIClient.ContributorsServiceAPI.FindAllChurnPullRequestContributors(m.serverContext). + Page(int32(m.queryOptions[0])).Limit(int32(m.queryOptions[1])). + Range_(int32(m.queryOptions[2])).RepoIds(strconv.Itoa(int(m.RepositoryInfo.Id))).Execute() + if err != nil { + return nil, err + } + + if r.StatusCode != 200 { + return nil, fmt.Errorf("HTTP status: %d", r.StatusCode) + } + + return resp.Data, nil +} + +// setupContributorsTable: sets the contributor table UI +func setupContributorsTable(contributors []client.DbPullRequestContributor) table.Model { + columns := []table.Column{ + {Title: "ID", Width: 5}, + {Title: "Name", Width: 20}, + } + + rows := []table.Row{} + + for i, contributor := range contributors { + rows = append(rows, table.Row{strconv.Itoa(i), contributor.AuthorLogin}) + } + + contributorTable := table.New( + table.WithColumns(columns), + table.WithRows(rows), + table.WithFocused(true), + table.WithHeight(15), + ) + + s := table.DefaultStyles() + s.Header = s.Header. + BorderStyle(lipgloss.NormalBorder()). + BorderBottom(true). + Bold(true) + + s.Selected = s.Selected. + Foreground(lipgloss.Color("#FFFFFF")). + Background(lipgloss.Color("#FF4500")) + + contributorTable.SetStyles(s) + return contributorTable +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} diff --git a/cmd/show/show.go b/cmd/show/show.go new file mode 100644 index 0000000..c7c59a2 --- /dev/null +++ b/cmd/show/show.go @@ -0,0 +1,85 @@ +// Package show contains the bootstrapping and tooling for the pizza show +// cobra command +package show + +import ( + "context" + "errors" + + client "github.com/open-sauced/go-api/client" + "github.com/open-sauced/pizza-cli/pkg/api" + "github.com/spf13/cobra" +) + +// Options are the options for the pizza show command including user +// defined configurations +type Options struct { + // RepoName is the git repo name to get statistics + RepoName string + + // Page is the page to be requested + Page int + + // Limit is the number of records to be retrieved + Limit int + + // Range is the number of days to take into account when retrieving statistics + Range int + + // APIClient is the api client to interface with open sauced api + APIClient *client.APIClient + + ServerContext context.Context +} + +const showLongDesc string = `WARNING: Proof of concept feature. + +The show command accepts the name of a git repository in the format 'owner/name' and uses OpenSauced api +to retrieve metrics of the repository to be displayed as a TUI.` + +// NewShowCommand returns a new cobra command for 'pizza show' +func NewShowCommand() *cobra.Command { + opts := &Options{} + + cmd := &cobra.Command{ + Use: "show repository-name [flags]", + Short: "Get visual metrics of a repository", + Long: showLongDesc, + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("must specify the URL of a git repository to analyze") + } + opts.RepoName = args[0] + + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + var endpoint string + customEndpoint, _ := cmd.Flags().GetString("endpoint") + if customEndpoint != "" { + endpoint = customEndpoint + } + + useBeta, _ := cmd.Flags().GetBool("beta") + if useBeta { + endpoint = api.BetaAPIEndpoint + } + + opts.APIClient = api.NewGoClient(endpoint) + opts.ServerContext = context.TODO() + return run(opts) + }, + } + + cmd.Flags().IntVarP(&opts.Range, "range", "r", 30, "The last N number of days to consider") + cmd.Flags().IntVarP(&opts.Limit, "limit", "l", 10, "The number of records to retrieve") + cmd.Flags().IntVarP(&opts.Page, "page", "p", 1, "The page number to retrieve") + + return cmd +} + +func run(opts *Options) error { + // Load the pizza TUI + err := pizzaTUI(opts) + return err +} diff --git a/cmd/show/styles.go b/cmd/show/styles.go new file mode 100644 index 0000000..6262be2 --- /dev/null +++ b/cmd/show/styles.go @@ -0,0 +1,42 @@ +package show + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// WindowSize stores the size of the terminal +var WindowSize tea.WindowSizeMsg + +// STYLES + +// Viewport: The viewport of the tui (my:2, mx:40) +var Viewport = lipgloss.NewStyle().Margin(1, 2) + +// Container: container styling (width: 80, py: 0, px: 5) +var Container = lipgloss.NewStyle().Width(80).Padding(0, 5) + +// WidgetContainer: container for tables, and graphs (py:2, px:2) +var WidgetContainer = lipgloss.NewStyle().Padding(2, 2) + +// SquareBorder: Style to draw a border around a section +var SquareBorder = lipgloss.NewStyle().BorderStyle(lipgloss.RoundedBorder()).BorderForeground(Color) + +// TextContainer: container for text +var TextContainer = lipgloss.NewStyle().Padding(1, 1) + +// TableTitle: The style for table titles (width:25, align-horizontal:center, bold:true) +var TableTitle = lipgloss.NewStyle().Width(25).AlignHorizontal(lipgloss.Center).Bold(true) + +// Color: the color palette (Light: #000000, Dark: #FF4500) +var Color = lipgloss.AdaptiveColor{Light: "#000000", Dark: "#FF4500"} + +// ActiveStyle: table when selected (border:normal, border-foreground:#FF4500) +var ActiveStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#FF4500")) + +// InactiveStyle: table when not selected (border:normal, border-foreground:#FFFFFF) +var InactiveStyle = lipgloss.NewStyle(). + BorderStyle(lipgloss.NormalBorder()). + BorderForeground(lipgloss.Color("#FFFFFF")) diff --git a/cmd/show/tui.go b/cmd/show/tui.go new file mode 100644 index 0000000..989d70b --- /dev/null +++ b/cmd/show/tui.go @@ -0,0 +1,85 @@ +package show + +import ( + "fmt" + + tea "github.com/charmbracelet/bubbletea" +) + +const ( + dashboardView = iota + contributorView +) + +type ( + // sessionState serves as the variable to reference when looking for which model the user is in + sessionState int +) + +// MainModel: the main model is the central state manager of the TUI, decides which model is focused based on certain commands +type MainModel struct { + state sessionState + dashboard tea.Model + contributor tea.Model + authorName string +} + +// View: the view of the TUI +func (m MainModel) View() string { + switch m.state { + case contributorView: + return m.contributor.View() + default: + return m.dashboard.View() + } +} + +// Init: initial IO before program start +func (m MainModel) Init() tea.Cmd { return nil } + +// Update: Handle IO and Commands +func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + var cmds []tea.Cmd + switch msg := msg.(type) { + case BackMsg: + m.state = dashboardView + case SelectMsg: + m.state = contributorView + m.authorName = msg.contributorName + } + + switch m.state { + case dashboardView: + newDashboard, newCmd := m.dashboard.Update(msg) + m.dashboard = newDashboard + cmd = newCmd + case contributorView: + // TODO: process cmd error if contributor fails to fetch + var err error + m.contributor, err = InitContributor(m.authorName) + if err != nil { + m.state = dashboardView + return m, func() tea.Msg { return ContributorErrMsg{name: m.authorName, err: err} } + } + newContributor, newCmd := m.contributor.Update(msg) + m.contributor = newContributor + cmd = newCmd + } + cmds = append(cmds, cmd) + return m, tea.Batch(cmds...) +} + +func pizzaTUI(opts *Options) error { + dashboardModel, err := InitDashboard(opts) + if err != nil { + return err + } + + model := MainModel{dashboard: dashboardModel} + if _, err := tea.NewProgram(model, tea.WithAltScreen()).Run(); err != nil { + return fmt.Errorf("Error running program: %s", err.Error()) + } + + return nil +} diff --git a/go.mod b/go.mod index d536864..15290a6 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.20 require ( github.com/charmbracelet/bubbles v0.16.1 + github.com/charmbracelet/bubbletea v0.24.2 github.com/charmbracelet/lipgloss v0.7.1 github.com/cli/browser v1.2.0 github.com/open-sauced/go-api/client v0.0.0-20230825180028-30fe67759eff @@ -28,6 +29,7 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.1 // indirect + github.com/open-sauced/go-api/client v0.0.0-20230825180028-30fe67759eff github.com/rivo/uniseg v0.2.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.9.0 // indirect diff --git a/go.sum b/go.sum index faaf92a..ae46c18 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,18 @@ github.com/cli/browser v1.2.0/go.mod h1:xFFnXLVcAyW9ni0cuo6NnrbCP75JxJ0RO7VtCBiH github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +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/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= +github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= +github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= +github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= +github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= +github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/cli/browser v1.2.0 h1:yvU7e9qf97kZqGFX6n2zJPHsmSObY9ske+iCvKelvXg= +github.com/cli/browser v1.2.0/go.mod h1:xFFnXLVcAyW9ni0cuo6NnrbCP75JxJ0RO7VtCBiH/oI= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -43,6 +55,28 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= +github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= +github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/open-sauced/go-api/client v0.0.0-20230825180028-30fe67759eff h1:sHfgCRtAg3xzNXoqqJ2MhDLwPj4GhC99ksDYC+K1BtM= +github.com/open-sauced/go-api/client v0.0.0-20230825180028-30fe67759eff/go.mod h1:W/TRuLUqYpMvkmElDUQvQ07xlxhK8TOfpwRh8SCAuNA= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= @@ -53,6 +87,9 @@ github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= diff --git a/pkg/api/constants.go b/pkg/api/constants.go new file mode 100644 index 0000000..5f4a0d0 --- /dev/null +++ b/pkg/api/constants.go @@ -0,0 +1,6 @@ +package api + +const ( + APIEndpoint = "https://api.opensauced.pizza" + BetaAPIEndpoint = "https://beta.api.opensauced.pizza" +) From 4cb029338a9656a0e851e5e9899b964724a885b5 Mon Sep 17 00:00:00 2001 From: k1nho Date: Sat, 9 Sep 2023 12:52:22 -0400 Subject: [PATCH 2/8] command flow --- cmd/root/root.go | 1 + cmd/show/contributors.go | 56 ++++++++++++++++++++-------------------- cmd/show/dashboard.go | 51 ++++++++++++++---------------------- cmd/show/tui.go | 38 +++++++++++++++++---------- 4 files changed, 74 insertions(+), 72 deletions(-) diff --git a/cmd/root/root.go b/cmd/root/root.go index 79166ea..0054ae7 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -12,6 +12,7 @@ import ( "github.com/open-sauced/pizza-cli/pkg/constants" "github.com/spf13/cobra" "github.com/open-sauced/pizza-cli/cmd/show" + "github.com/open-sauced/pizza-cli/cmd/version" "github.com/open-sauced/pizza-cli/pkg/api" ) diff --git a/cmd/show/contributors.go b/cmd/show/contributors.go index 3e04ce2..f1b465d 100644 --- a/cmd/show/contributors.go +++ b/cmd/show/contributors.go @@ -13,9 +13,11 @@ import ( // ContributorModel holds all the information related to a contributor type ContributorModel struct { - username string - userInfo *client.DbUser - userPrs []client.DbPullRequest + username string + userInfo *client.DbUser + userPrs []client.DbPullRequest + APIClient *client.APIClient + serverContext context.Context } type ( @@ -30,16 +32,12 @@ type ( ) // InitContributor: initializes the contributorModel -func InitContributor(contributorName string) (ContributorModel, error) { - var contributorModel ContributorModel - contributorModel.username = contributorName +func InitContributor(opts *Options) (tea.Model, error) { + var model ContributorModel + model.APIClient = opts.APIClient + model.serverContext = opts.ServerContext - err := contributorModel.fetchUser() - if err != nil { - return contributorModel, err - } - - return contributorModel, nil + return model, nil } func (m ContributorModel) Init() tea.Cmd { return nil } @@ -47,6 +45,13 @@ func (m ContributorModel) Init() tea.Cmd { return nil } func (m ContributorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { + case SelectMsg: + m.username = msg.contributorName + model, err := m.fetchUser() + if err != nil { + return m, func() tea.Msg { return ContributorErrMsg{name: msg.contributorName, err: err} } + } + return model, func() tea.Msg { return SuccessMsg{} } case tea.KeyMsg: switch msg.String() { case "backspace": @@ -63,16 +68,16 @@ func (m ContributorModel) View() string { } // fetchUser: fetches all the user information (general info, and pull requests) -func (m *ContributorModel) fetchUser() error { +func (m *ContributorModel) fetchUser() (tea.Model, error) { var ( wg sync.WaitGroup errChan = make(chan error, 2) ) - wg.Add(2) + wg.Add(1) go func() { defer wg.Done() - userInfo, err := fetchContributorInfo(m.username) + userInfo, err := m.fetchContributorInfo(m.username) if err != nil { errChan <- err return @@ -81,9 +86,10 @@ func (m *ContributorModel) fetchUser() error { }() + wg.Add(1) go func() { defer wg.Done() - userPRs, err := fetchContributorPRs(m.username) + userPRs, err := m.fetchContributorPRs(m.username) if err != nil { errChan <- err return @@ -98,19 +104,16 @@ func (m *ContributorModel) fetchUser() error { for err := range errChan { allErrors = errors.Join(allErrors, err) } - return allErrors + return m, allErrors } - return nil + return m, nil } // fetchContributorInfo: fetches the contributor info -func fetchContributorInfo(name string) (*client.DbUser, error) { - config := client.NewConfiguration() - apiClient := client.NewAPIClient(config) - ctx := context.WithValue(context.Background(), client.ContextServerIndex, 1) +func (m *ContributorModel) fetchContributorInfo(name string) (*client.DbUser, error) { - resp, r, err := apiClient.UserServiceAPI.FindOneUserByUserame(ctx, name).Execute() + resp, r, err := m.APIClient.UserServiceAPI.FindOneUserByUserame(m.serverContext, name).Execute() if err != nil { return nil, err } @@ -123,12 +126,9 @@ func fetchContributorInfo(name string) (*client.DbUser, error) { } // fetchContributorPRs: fetches the contributor pull requests -func fetchContributorPRs(name string) ([]client.DbPullRequest, error) { - config := client.NewConfiguration() - apiClient := client.NewAPIClient(config) - ctx := context.WithValue(context.Background(), client.ContextServerIndex, 1) +func (m *ContributorModel) fetchContributorPRs(name string) ([]client.DbPullRequest, error) { - resp, r, err := apiClient.UserServiceAPI.FindContributorPullRequests(ctx, name).Execute() + resp, r, err := m.APIClient.UserServiceAPI.FindContributorPullRequests(m.serverContext, name).Execute() if err != nil { return nil, err } diff --git a/cmd/show/dashboard.go b/cmd/show/dashboard.go index e6a056f..8a4c6a2 100644 --- a/cmd/show/dashboard.go +++ b/cmd/show/dashboard.go @@ -37,14 +37,14 @@ type SelectMsg struct { } // FetchRepoInfo: initializes the dashboard model -func InitDashboard(opts *Options) (DashboardModel, error) { - model := DashboardModel{} +func InitDashboard(opts *Options) (tea.Model, error) { + var model DashboardModel err := validateShowQuery(opts) if err != nil { return model, err } - ownerRepo := strings.Split(opts.RepoName, "/") + ownerRepo := strings.Split(opts.RepoName, "/") resp, r, err := opts.APIClient.RepositoryServiceAPI.FindOneByOwnerAndRepo(opts.ServerContext, ownerRepo[0], ownerRepo[1]).Execute() if err != nil { return model, err @@ -60,7 +60,7 @@ func InitDashboard(opts *Options) (DashboardModel, error) { model.APIClient = opts.APIClient model.serverContext = opts.ServerContext - // Fetching all contributors + // fetching the contributor tables err = model.FetchAllContributors() if err != nil { return model, err @@ -69,22 +69,28 @@ func InitDashboard(opts *Options) (DashboardModel, error) { return model, nil } -func (m DashboardModel) Init() tea.Cmd { return nil } +func (m DashboardModel) Init() tea.Cmd { + return nil +} func (m DashboardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { - case ContributorErrMsg: - m.contributorErr = fmt.Sprintf("🚧 could not fetch %s: %s", msg.name, msg.err.Error()) - default: - m.contributorErr = "" - case tea.WindowSizeMsg: WindowSize = msg width, _ := m.alumniContributorsTable.Width(), m.alumniContributorsTable.Height() m.alumniContributorsTable.SetWidth(max(msg.Width-width, 5)) m.newContributorsTable.SetWidth(max(msg.Width-width, 5)) + case ErrMsg: + fmt.Printf("Failed to retrieve contributors table data: %s", msg.err.Error()) + return m, tea.Quit + + case ContributorErrMsg: + m.contributorErr = fmt.Sprintf("🚧 could not fetch %s: %s", msg.name, msg.err.Error()) + default: + m.contributorErr = "" + case tea.KeyMsg: switch msg.String() { case "right", "l": @@ -192,27 +198,9 @@ func (m *DashboardModel) drawDashboardView() string { if WindowSize.Width == 0 { return "Loading..." } - var wg sync.WaitGroup - - wg.Add(3) - var titleView, repoInfoView, metricsView string - go func() { - defer wg.Done() - titleView = m.drawTitle() - }() - - go func() { - defer wg.Done() - repoInfoView = m.drawRepositoryInfo() - }() - - go func() { - defer wg.Done() - metricsView = m.drawMetrics() - }() - wg.Wait() - + titleView, repoInfoView, metricsView := m.drawTitle(), m.drawRepositoryInfo(), m.drawMetrics() mainView := lipgloss.JoinVertical(lipgloss.Center, titleView, repoInfoView, metricsView, m.contributorErr) + _, h := lipgloss.Size(mainView) if WindowSize.Height < h { contentLeft := lipgloss.JoinVertical(lipgloss.Center, titleView, repoInfoView) @@ -252,7 +240,7 @@ func (m *DashboardModel) FetchAllContributors() error { wg sync.WaitGroup ) - wg.Add(2) + wg.Add(1) go func() { defer wg.Done() newContributors, err := m.FetchNewContributors() @@ -263,6 +251,7 @@ func (m *DashboardModel) FetchAllContributors() error { m.newContributorsTable = setupContributorsTable(newContributors) }() + wg.Add(1) go func() { defer wg.Done() alumniContributors, err := m.FetchAlumniContributors() diff --git a/cmd/show/tui.go b/cmd/show/tui.go index 989d70b..8fbb351 100644 --- a/cmd/show/tui.go +++ b/cmd/show/tui.go @@ -14,6 +14,14 @@ const ( type ( // sessionState serves as the variable to reference when looking for which model the user is in sessionState int + + // SuccessMsg: message to represent success when fetching + SuccessMsg struct{} + + // ErrMsg: this message represents an error when fetching + ErrMsg struct { + err error + } ) // MainModel: the main model is the central state manager of the TUI, decides which model is focused based on certain commands @@ -21,7 +29,6 @@ type MainModel struct { state sessionState dashboard tea.Model contributor tea.Model - authorName string } // View: the view of the TUI @@ -35,18 +42,26 @@ func (m MainModel) View() string { } // Init: initial IO before program start -func (m MainModel) Init() tea.Cmd { return nil } +func (m MainModel) Init() tea.Cmd { + return nil +} // Update: Handle IO and Commands func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd switch msg := msg.(type) { + case tea.WindowSizeMsg: + WindowSize.Width = msg.Width + WindowSize.Height = msg.Height case BackMsg: m.state = dashboardView case SelectMsg: + newContributor, newCmd := m.contributor.Update(msg) + m.contributor = newContributor + cmds = append(cmds, newCmd) + case SuccessMsg: m.state = contributorView - m.authorName = msg.contributorName } switch m.state { @@ -55,15 +70,7 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.dashboard = newDashboard cmd = newCmd case contributorView: - // TODO: process cmd error if contributor fails to fetch - var err error - m.contributor, err = InitContributor(m.authorName) - if err != nil { - m.state = dashboardView - return m, func() tea.Msg { return ContributorErrMsg{name: m.authorName, err: err} } - } - newContributor, newCmd := m.contributor.Update(msg) - m.contributor = newContributor + _, newCmd := m.contributor.Update(msg) cmd = newCmd } cmds = append(cmds, cmd) @@ -76,7 +83,12 @@ func pizzaTUI(opts *Options) error { return err } - model := MainModel{dashboard: dashboardModel} + contributorModel, err := InitContributor(opts) + if err != nil { + return err + } + + model := MainModel{dashboard: dashboardModel, contributor: contributorModel} if _, err := tea.NewProgram(model, tea.WithAltScreen()).Run(); err != nil { return fmt.Errorf("Error running program: %s", err.Error()) } From 6ee03ce840325d39fa261bbc99c39617556e1df9 Mon Sep 17 00:00:00 2001 From: k1nho Date: Sat, 9 Sep 2023 12:57:57 -0400 Subject: [PATCH 3/8] tidy --- cmd/root/root.go | 4 +--- go.mod | 2 -- go.sum | 39 +-------------------------------------- 3 files changed, 2 insertions(+), 43 deletions(-) diff --git a/cmd/root/root.go b/cmd/root/root.go index 0054ae7..ab86928 100644 --- a/cmd/root/root.go +++ b/cmd/root/root.go @@ -8,12 +8,10 @@ import ( "github.com/open-sauced/pizza-cli/cmd/bake" "github.com/open-sauced/pizza-cli/cmd/insights" repoquery "github.com/open-sauced/pizza-cli/cmd/repo-query" + "github.com/open-sauced/pizza-cli/cmd/show" "github.com/open-sauced/pizza-cli/cmd/version" "github.com/open-sauced/pizza-cli/pkg/constants" "github.com/spf13/cobra" - "github.com/open-sauced/pizza-cli/cmd/show" - "github.com/open-sauced/pizza-cli/cmd/version" - "github.com/open-sauced/pizza-cli/pkg/api" ) // NewRootCommand bootstraps a new root cobra command for the pizza CLI diff --git a/go.mod b/go.mod index 15290a6..4fcfee1 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,6 @@ require ( require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/bubbletea v0.24.1 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/google/uuid v1.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -29,7 +28,6 @@ require ( github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.1 // indirect - github.com/open-sauced/go-api/client v0.0.0-20230825180028-30fe67759eff github.com/rivo/uniseg v0.2.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.9.0 // indirect diff --git a/go.sum b/go.sum index ae46c18..9e49805 100644 --- a/go.sum +++ b/go.sum @@ -3,19 +3,6 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= -github.com/charmbracelet/bubbletea v0.24.1 h1:LpdYfnu+Qc6XtvMz6d/6rRY71yttHTP5HtrjMgWvixc= -github.com/charmbracelet/bubbletea v0.24.1/go.mod h1:rK3g/2+T8vOSEkNHvtq40umJpeVYDn6bLaqbgzhL/hg= -github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= -github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= -github.com/cli/browser v1.2.0 h1:yvU7e9qf97kZqGFX6n2zJPHsmSObY9ske+iCvKelvXg= -github.com/cli/browser v1.2.0/go.mod h1:xFFnXLVcAyW9ni0cuo6NnrbCP75JxJ0RO7VtCBiH/oI= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= -github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -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/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= -github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= @@ -24,6 +11,7 @@ github.com/cli/browser v1.2.0 h1:yvU7e9qf97kZqGFX6n2zJPHsmSObY9ske+iCvKelvXg= github.com/cli/browser v1.2.0/go.mod h1:xFFnXLVcAyW9ni0cuo6NnrbCP75JxJ0RO7VtCBiH/oI= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -55,28 +43,6 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -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/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= -github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= -github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= -github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= -github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= -github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= -github.com/open-sauced/go-api/client v0.0.0-20230825180028-30fe67759eff h1:sHfgCRtAg3xzNXoqqJ2MhDLwPj4GhC99ksDYC+K1BtM= -github.com/open-sauced/go-api/client v0.0.0-20230825180028-30fe67759eff/go.mod h1:W/TRuLUqYpMvkmElDUQvQ07xlxhK8TOfpwRh8SCAuNA= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= @@ -87,9 +53,6 @@ github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s= From b1289a1a45f6b2a9715907b0ad396e3a3f450205 Mon Sep 17 00:00:00 2001 From: k1nho Date: Sat, 9 Sep 2023 13:15:28 -0400 Subject: [PATCH 4/8] fix glitch effect on resize --- cmd/show/dashboard.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cmd/show/dashboard.go b/cmd/show/dashboard.go index 8a4c6a2..5d6f6af 100644 --- a/cmd/show/dashboard.go +++ b/cmd/show/dashboard.go @@ -78,9 +78,6 @@ func (m DashboardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: WindowSize = msg - width, _ := m.alumniContributorsTable.Width(), m.alumniContributorsTable.Height() - m.alumniContributorsTable.SetWidth(max(msg.Width-width, 5)) - m.newContributorsTable.SetWidth(max(msg.Width-width, 5)) case ErrMsg: fmt.Printf("Failed to retrieve contributors table data: %s", msg.err.Error()) From ad8609abfa7258818452b26a4e4b3e112d9e2f7f Mon Sep 17 00:00:00 2001 From: k1nho Date: Sat, 9 Sep 2023 14:09:17 -0400 Subject: [PATCH 5/8] remove unused max --- cmd/show/contributors.go | 2 -- cmd/show/dashboard.go | 14 ++------------ cmd/show/show.go | 2 +- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/cmd/show/contributors.go b/cmd/show/contributors.go index f1b465d..9fa2677 100644 --- a/cmd/show/contributors.go +++ b/cmd/show/contributors.go @@ -112,7 +112,6 @@ func (m *ContributorModel) fetchUser() (tea.Model, error) { // fetchContributorInfo: fetches the contributor info func (m *ContributorModel) fetchContributorInfo(name string) (*client.DbUser, error) { - resp, r, err := m.APIClient.UserServiceAPI.FindOneUserByUserame(m.serverContext, name).Execute() if err != nil { return nil, err @@ -127,7 +126,6 @@ func (m *ContributorModel) fetchContributorInfo(name string) (*client.DbUser, er // fetchContributorPRs: fetches the contributor pull requests func (m *ContributorModel) fetchContributorPRs(name string) ([]client.DbPullRequest, error) { - resp, r, err := m.APIClient.UserServiceAPI.FindContributorPullRequests(m.serverContext, name).Execute() if err != nil { return nil, err diff --git a/cmd/show/dashboard.go b/cmd/show/dashboard.go index 5d6f6af..4310d45 100644 --- a/cmd/show/dashboard.go +++ b/cmd/show/dashboard.go @@ -101,17 +101,14 @@ func (m DashboardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case "q", "esc", "ctrl+c", "ctrl+d": return m, tea.Quit case "enter": - var contributorName string switch m.tableView { case newContributorsView: if len(m.newContributorsTable.Rows()) > 0 { - contributorName = m.newContributorsTable.SelectedRow()[1] - return m, func() tea.Msg { return SelectMsg{contributorName} } + return m, func() tea.Msg { return SelectMsg{contributorName: m.newContributorsTable.SelectedRow()[1]} } } case alumniContributorsView: if len(m.alumniContributorsTable.Rows()) > 0 { - contributorName = m.alumniContributorsTable.SelectedRow()[1] - return m, func() tea.Msg { return SelectMsg{contributorName} } + return m, func() tea.Msg { return SelectMsg{contributorName: m.alumniContributorsTable.SelectedRow()[1]} } } } } @@ -337,10 +334,3 @@ func setupContributorsTable(contributors []client.DbPullRequestContributor) tabl contributorTable.SetStyles(s) return contributorTable } - -func max(a, b int) int { - if a > b { - return a - } - return b -} diff --git a/cmd/show/show.go b/cmd/show/show.go index c7c59a2..82ab4dc 100644 --- a/cmd/show/show.go +++ b/cmd/show/show.go @@ -34,7 +34,7 @@ type Options struct { const showLongDesc string = `WARNING: Proof of concept feature. -The show command accepts the name of a git repository in the format 'owner/name' and uses OpenSauced api +The show command accepts the name of a git repository and uses OpenSauced api to retrieve metrics of the repository to be displayed as a TUI.` // NewShowCommand returns a new cobra command for 'pizza show' From 8a5bde6b1b8c32add4aeee6a7d8653d92cc582f4 Mon Sep 17 00:00:00 2001 From: k1nho Date: Sun, 10 Sep 2023 12:56:44 -0400 Subject: [PATCH 6/8] accept https://github.com/owner/name format and owner/name format --- cmd/show/dashboard.go | 11 +---------- cmd/show/show.go | 14 ++++++++++++-- pkg/utils/github.go | 39 +++++++++++++++++++++++---------------- 3 files changed, 36 insertions(+), 28 deletions(-) diff --git a/cmd/show/dashboard.go b/cmd/show/dashboard.go index 4310d45..cda0ea5 100644 --- a/cmd/show/dashboard.go +++ b/cmd/show/dashboard.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "strconv" - "strings" "sync" "github.com/charmbracelet/bubbles/table" @@ -44,8 +43,7 @@ func InitDashboard(opts *Options) (tea.Model, error) { return model, err } - ownerRepo := strings.Split(opts.RepoName, "/") - resp, r, err := opts.APIClient.RepositoryServiceAPI.FindOneByOwnerAndRepo(opts.ServerContext, ownerRepo[0], ownerRepo[1]).Execute() + resp, r, err := opts.APIClient.RepositoryServiceAPI.FindOneByOwnerAndRepo(opts.ServerContext, opts.Owner, opts.RepoName).Execute() if err != nil { return model, err } @@ -206,13 +204,6 @@ func (m *DashboardModel) drawDashboardView() string { // validateShowQuery: validates fields set to query the contributor tables func validateShowQuery(opts *Options) error { - if opts.RepoName == "" { - return errors.New("no repository was provided") - } - - if len(strings.Split(opts.RepoName, "/")) != 2 { - return errors.New("wrong repository name format. Must be in the form owner/name, i.e(open-sauced/pizza)") - } if opts.Limit < 1 { return errors.New("--limit flag must be a positive integer value") } diff --git a/cmd/show/show.go b/cmd/show/show.go index 82ab4dc..662b268 100644 --- a/cmd/show/show.go +++ b/cmd/show/show.go @@ -8,13 +8,17 @@ import ( client "github.com/open-sauced/go-api/client" "github.com/open-sauced/pizza-cli/pkg/api" + "github.com/open-sauced/pizza-cli/pkg/utils" "github.com/spf13/cobra" ) // Options are the options for the pizza show command including user // defined configurations type Options struct { - // RepoName is the git repo name to get statistics + // Owner: the owner of the repository + Owner string + + // RepoName: the name of the repository RepoName string // Page is the page to be requested @@ -49,7 +53,13 @@ func NewShowCommand() *cobra.Command { if len(args) != 1 { return errors.New("must specify the URL of a git repository to analyze") } - opts.RepoName = args[0] + + owner, name, err := utils.GetOwnerAndRepoFromURL(args[0]) + if err != nil { + return err + } + opts.Owner = owner + opts.RepoName = name return nil }, diff --git a/pkg/utils/github.go b/pkg/utils/github.go index 83a8ac1..68be868 100644 --- a/pkg/utils/github.go +++ b/pkg/utils/github.go @@ -2,27 +2,34 @@ package utils import ( "fmt" + "net/url" "strings" ) -func GetOwnerAndRepoFromURL(url string) (owner, repo string, err error) { - if !strings.HasPrefix(url, "https://github.com/") { - return "", "", fmt.Errorf("invalid URL: %s", url) - } - - // Remove the "https://github.com/" prefix from the URL - url = strings.TrimPrefix(url, "https://github.com/") +// GetOwnerAndRepoFromURL: extracts the owner and repository name +func GetOwnerAndRepoFromURL(input string) (owner, repo string, err error) { + var repoOwner, repoName string - // Split the remaining URL path into segments - segments := strings.Split(url, "/") + // check (https://github.com/owner/repo) format + u, err := url.Parse(input) + if err == nil && u.Host == "github.com" { + path := strings.Trim(u.Path, "/") + parts := strings.Split(path, "/") + if len(parts) != 2 { + return "", "", fmt.Errorf("Invalid URL: %s", input) + } + repoOwner = parts[0] + repoName = parts[1] + return repoOwner, repoName, nil + } - // The first segment is the owner, and the second segment is the repository name - if len(segments) >= 2 { - owner = segments[0] - repo = segments[1] - } else { - return "", "", fmt.Errorf("invalid URL: %s", url) + // check (owner/repo) format + parts := strings.Split(input, "/") + if len(parts) != 2 { + return "", "", fmt.Errorf("Invalid URL: %s", input) } + repoOwner = parts[0] + repoName = parts[1] - return owner, repo, nil + return repoOwner, repoName, nil } From c6d334a073abf75edf045f829a996afeb0cbf0f4 Mon Sep 17 00:00:00 2001 From: k1nho Date: Thu, 21 Sep 2023 15:24:46 -0400 Subject: [PATCH 7/8] improving pull request list display --- cmd/show/{styles.go => constants.go} | 25 ++++- cmd/show/contributors.go | 154 +++++++++++++++++++-------- cmd/show/dashboard.go | 3 +- cmd/show/tui.go | 10 +- go.mod | 2 + go.sum | 5 + 6 files changed, 143 insertions(+), 56 deletions(-) rename cmd/show/{styles.go => constants.go} (59%) diff --git a/cmd/show/styles.go b/cmd/show/constants.go similarity index 59% rename from cmd/show/styles.go rename to cmd/show/constants.go index 6262be2..9c68085 100644 --- a/cmd/show/styles.go +++ b/cmd/show/constants.go @@ -1,6 +1,8 @@ package show import ( + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) @@ -8,9 +10,13 @@ import ( // WindowSize stores the size of the terminal var WindowSize tea.WindowSizeMsg -// STYLES +// Keymaps +var OpenPR = key.NewBinding(key.WithKeys("O"), key.WithHelp("O", "open pr")) +var BackToDashboard = key.NewBinding(key.WithKeys("B"), key.WithHelp("B", "back")) +var ToggleHelpMenu = key.NewBinding(key.WithKeys("H"), key.WithHelp("H", "toggle help")) -// Viewport: The viewport of the tui (my:2, mx:40) +// STYLES +// Viewport: The viewport of the tui (my:2, mx:2) var Viewport = lipgloss.NewStyle().Margin(1, 2) // Container: container styling (width: 80, py: 0, px: 5) @@ -40,3 +46,18 @@ var ActiveStyle = lipgloss.NewStyle(). var InactiveStyle = lipgloss.NewStyle(). BorderStyle(lipgloss.NormalBorder()). BorderForeground(lipgloss.Color("#FFFFFF")) + +// ItemStyle: style applied to items in a list.Model +var ItemStyle = lipgloss.NewStyle().PaddingLeft(4) + +// SelectedItemStyle: style applied when the item is selected in a list.Model +var SelectedItemStyle = lipgloss.NewStyle().PaddingLeft(2).Foreground(Color) + +// ListItemTitle: style for the list.Model title +var ListItemTitleStyle = lipgloss.NewStyle().MarginLeft(2) + +// PaginationStyle: style for pagination of list.Model +var PaginationStyle = list.DefaultStyles().PaginationStyle.PaddingLeft(4) + +// HelpStyle: style for help menu +var HelpStyle = list.DefaultStyles().HelpStyle.PaddingLeft(4).PaddingBottom(1) diff --git a/cmd/show/contributors.go b/cmd/show/contributors.go index 9fa2677..03103f9 100644 --- a/cmd/show/contributors.go +++ b/cmd/show/contributors.go @@ -4,18 +4,62 @@ import ( "context" "errors" "fmt" + "io" + "strings" "sync" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" + "github.com/cli/browser" client "github.com/open-sauced/go-api/client" ) +// prItem: type for pull request to satisfy the list.Item interface +type prItem client.DbPullRequest + +func (i prItem) FilterValue() string { return i.Title } +func (i prItem) GetRepoName() string { + if i.FullName != nil { + return *i.FullName + } + return "" +} + +type itemDelegate struct{} + +func (d itemDelegate) Height() int { return 1 } +func (d itemDelegate) Spacing() int { return 1 } +func (d itemDelegate) Update(_ tea.Msg, _ *list.Model) tea.Cmd { return nil } +func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list.Item) { + i, ok := listItem.(prItem) + if !ok { + return + } + + prTitle := i.Title + if len(prTitle) >= 60 { + prTitle = fmt.Sprintf("%s...", prTitle[:60]) + } + + str := fmt.Sprintf("#%d %s\n%s\n(%s)", i.Number, i.GetRepoName(), prTitle, i.State) + + fn := ItemStyle.Render + if index == m.Index() { + fn = func(s ...string) string { + return SelectedItemStyle.Render("🍕 " + strings.Join(s, " ")) + } + } + + fmt.Fprint(w, fn(str)) +} + // ContributorModel holds all the information related to a contributor type ContributorModel struct { username string userInfo *client.DbUser - userPrs []client.DbPullRequest + prList list.Model APIClient *client.APIClient serverContext context.Context } @@ -45,6 +89,8 @@ func (m ContributorModel) Init() tea.Cmd { return nil } func (m ContributorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd switch msg := msg.(type) { + case tea.WindowSizeMsg: + WindowSize = msg case SelectMsg: m.username = msg.contributorName model, err := m.fetchUser() @@ -54,12 +100,25 @@ func (m ContributorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return model, func() tea.Msg { return SuccessMsg{} } case tea.KeyMsg: switch msg.String() { - case "backspace": + case "B": return m, func() tea.Msg { return BackMsg{} } - case "q", "esc", "ctrl+c", "ctrl+d": + case "H": + m.prList.SetShowHelp(!m.prList.ShowHelp()) + return m, nil + case "O": + pr, ok := m.prList.SelectedItem().(prItem) + if ok { + err := browser.OpenURL(fmt.Sprintf("https://github.com/%s/pull/%d", pr.GetRepoName(), pr.Number)) + if err != nil { + fmt.Println("could not open pull request in browser") + } + } + case "q", "ctrl+c", "ctrl+d": return m, tea.Quit + } } + m.prList, cmd = m.prList.Update(msg) return m, cmd } @@ -77,24 +136,21 @@ func (m *ContributorModel) fetchUser() (tea.Model, error) { wg.Add(1) go func() { defer wg.Done() - userInfo, err := m.fetchContributorInfo(m.username) + err := m.fetchContributorInfo(m.username) if err != nil { errChan <- err return } - m.userInfo = userInfo - }() wg.Add(1) go func() { defer wg.Done() - userPRs, err := m.fetchContributorPRs(m.username) + err := m.fetchContributorPRs(m.username) if err != nil { errChan <- err return } - m.userPrs = userPRs }() wg.Wait() @@ -111,36 +167,68 @@ func (m *ContributorModel) fetchUser() (tea.Model, error) { } // fetchContributorInfo: fetches the contributor info -func (m *ContributorModel) fetchContributorInfo(name string) (*client.DbUser, error) { +func (m *ContributorModel) fetchContributorInfo(name string) error { resp, r, err := m.APIClient.UserServiceAPI.FindOneUserByUserame(m.serverContext, name).Execute() if err != nil { - return nil, err + return err } if r.StatusCode != 200 { - return nil, fmt.Errorf("HTTP failed: %d", r.StatusCode) + return fmt.Errorf("HTTP failed: %d", r.StatusCode) } - return resp, nil + m.userInfo = resp + return nil } -// fetchContributorPRs: fetches the contributor pull requests -func (m *ContributorModel) fetchContributorPRs(name string) ([]client.DbPullRequest, error) { - resp, r, err := m.APIClient.UserServiceAPI.FindContributorPullRequests(m.serverContext, name).Execute() +// fetchContributorPRs: fetches the contributor pull requests and creates pull request list +func (m *ContributorModel) fetchContributorPRs(name string) error { + resp, r, err := m.APIClient.UserServiceAPI.FindContributorPullRequests(m.serverContext, name).Limit(10).Execute() if err != nil { - return nil, err + return err } if r.StatusCode != 200 { - return nil, fmt.Errorf("HTTP failed: %d", r.StatusCode) + return fmt.Errorf("HTTP failed: %d", r.StatusCode) + } + + // create contributor pull request list + var items []list.Item + for _, pr := range resp.Data { + items = append(items, prItem(pr)) } - return resp.Data, nil + l := list.New(items, itemDelegate{}, WindowSize.Width, 14) + l.Title = "✨ Latest Pull Requests" + l.Styles.Title = ListItemTitleStyle + l.Styles.HelpStyle = HelpStyle + l.SetShowStatusBar(false) + l.SetStatusBarItemName("pull request", "pull requests") + l.AdditionalShortHelpKeys = func() []key.Binding { + return []key.Binding{ + OpenPR, + BackToDashboard, + ToggleHelpMenu, + } + } + + m.prList = l + return nil } // drawContributorView: view of the contributor model func (m *ContributorModel) drawContributorView() string { - return Viewport.Copy().Render(lipgloss.JoinVertical(lipgloss.Center, m.drawContributorInfo(), m.drawPullRequests())) + contributorInfo := m.drawContributorInfo() + + contributorView := lipgloss.JoinVertical(lipgloss.Left, lipgloss.NewStyle().PaddingLeft(2).Render(contributorInfo), + WidgetContainer.Render(m.prList.View())) + + _, h := lipgloss.Size(contributorView) + if WindowSize.Height < h { + contributorView = lipgloss.JoinHorizontal(lipgloss.Center, contributorInfo, m.prList.View()) + } + + return contributorView } // drawContributorInfo: view of the contributor info (open issues, pr velocity, pr count, maintainer) @@ -158,31 +246,3 @@ func (m *ContributorModel) drawContributorInfo() string { return SquareBorder.Render(contributorView) } - -// drawPullRequests: view of the contributor pull requests (draws the last 5 pull requests) -func (m *ContributorModel) drawPullRequests() string { - if len(m.userPrs) == 0 { - return "" - } - - pullRequests := []string{} - var numberOfPrs int - - if len(m.userPrs) > 5 { - numberOfPrs = 5 - } else { - numberOfPrs = len(m.userPrs) - } - - for i := 0; i < numberOfPrs; i++ { - prContainer := TextContainer.Render(fmt.Sprintf("#%d %s\n%s\n(%s)", m.userPrs[i].Number, m.userPrs[i].GetFullName(), - m.userPrs[i].Title, m.userPrs[i].State)) - pullRequests = append(pullRequests, prContainer) - } - - formattedPrs := lipgloss.JoinVertical(lipgloss.Left, pullRequests...) - title := lipgloss.NewStyle().AlignHorizontal(lipgloss.Center).Render("✨ Latest Pull Requests") - - pullRequestView := lipgloss.JoinVertical(lipgloss.Center, title, formattedPrs) - return WidgetContainer.Render(pullRequestView) -} diff --git a/cmd/show/dashboard.go b/cmd/show/dashboard.go index cda0ea5..14efb55 100644 --- a/cmd/show/dashboard.go +++ b/cmd/show/dashboard.go @@ -196,7 +196,8 @@ func (m *DashboardModel) drawDashboardView() string { _, h := lipgloss.Size(mainView) if WindowSize.Height < h { contentLeft := lipgloss.JoinVertical(lipgloss.Center, titleView, repoInfoView) - mainView = lipgloss.JoinHorizontal(lipgloss.Center, contentLeft, metricsView) + contentRight := lipgloss.JoinVertical(lipgloss.Center, metricsView, m.contributorErr) + mainView = lipgloss.JoinHorizontal(lipgloss.Center, contentLeft, contentRight) } frame := Viewport.Render(mainView) return frame diff --git a/cmd/show/tui.go b/cmd/show/tui.go index 8fbb351..141367e 100644 --- a/cmd/show/tui.go +++ b/cmd/show/tui.go @@ -51,17 +51,14 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd switch msg := msg.(type) { - case tea.WindowSizeMsg: - WindowSize.Width = msg.Width - WindowSize.Height = msg.Height case BackMsg: m.state = dashboardView + case SuccessMsg: + m.state = contributorView case SelectMsg: newContributor, newCmd := m.contributor.Update(msg) m.contributor = newContributor cmds = append(cmds, newCmd) - case SuccessMsg: - m.state = contributorView } switch m.state { @@ -70,7 +67,8 @@ func (m MainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.dashboard = newDashboard cmd = newCmd case contributorView: - _, newCmd := m.contributor.Update(msg) + newContributor, newCmd := m.contributor.Update(msg) + m.contributor = newContributor cmd = newCmd } cmds = append(cmds, cmd) diff --git a/go.mod b/go.mod index 4fcfee1..c589872 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( ) require ( + github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/google/uuid v1.3.0 // indirect @@ -29,6 +30,7 @@ require ( github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.1 // indirect github.com/rivo/uniseg v0.2.0 // indirect + github.com/sahilm/fuzzy v0.1.0 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.9.0 // indirect golang.org/x/text v0.3.8 // indirect diff --git a/go.sum b/go.sum index 9e49805..eadba7c 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +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/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= @@ -17,6 +19,7 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 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/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= @@ -44,6 +47,8 @@ github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= From 283e0a76d43217d163319d62293a0cf82614cc78 Mon Sep 17 00:00:00 2001 From: k1nho Date: Tue, 26 Sep 2023 23:21:02 -0400 Subject: [PATCH 8/8] fix keymap collision when filtering --- cmd/show/contributors.go | 28 ++++++++++++++++++---------- cmd/show/show.go | 3 ++- pkg/api/constants.go | 6 ------ pkg/utils/github.go | 4 ++-- 4 files changed, 22 insertions(+), 19 deletions(-) delete mode 100644 pkg/api/constants.go diff --git a/cmd/show/contributors.go b/cmd/show/contributors.go index 03103f9..5872940 100644 --- a/cmd/show/contributors.go +++ b/cmd/show/contributors.go @@ -101,21 +101,28 @@ func (m ContributorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { case tea.KeyMsg: switch msg.String() { case "B": - return m, func() tea.Msg { return BackMsg{} } + if !m.prList.SettingFilter() { + return m, func() tea.Msg { return BackMsg{} } + } case "H": - m.prList.SetShowHelp(!m.prList.ShowHelp()) - return m, nil + if !m.prList.SettingFilter() { + m.prList.SetShowHelp(!m.prList.ShowHelp()) + return m, nil + } case "O": - pr, ok := m.prList.SelectedItem().(prItem) - if ok { - err := browser.OpenURL(fmt.Sprintf("https://github.com/%s/pull/%d", pr.GetRepoName(), pr.Number)) - if err != nil { - fmt.Println("could not open pull request in browser") + if !m.prList.SettingFilter() { + pr, ok := m.prList.SelectedItem().(prItem) + if ok { + err := browser.OpenURL(fmt.Sprintf("https://github.com/%s/pull/%d", pr.GetRepoName(), pr.Number)) + if err != nil { + fmt.Println("could not open pull request in browser") + } } } case "q", "ctrl+c", "ctrl+d": - return m, tea.Quit - + if !m.prList.SettingFilter() { + return m, tea.Quit + } } } m.prList, cmd = m.prList.Update(msg) @@ -202,6 +209,7 @@ func (m *ContributorModel) fetchContributorPRs(name string) error { l.Title = "✨ Latest Pull Requests" l.Styles.Title = ListItemTitleStyle l.Styles.HelpStyle = HelpStyle + l.Styles.NoItems = ItemStyle l.SetShowStatusBar(false) l.SetStatusBarItemName("pull request", "pull requests") l.AdditionalShortHelpKeys = func() []key.Binding { diff --git a/cmd/show/show.go b/cmd/show/show.go index 662b268..9ab79e0 100644 --- a/cmd/show/show.go +++ b/cmd/show/show.go @@ -8,6 +8,7 @@ import ( client "github.com/open-sauced/go-api/client" "github.com/open-sauced/pizza-cli/pkg/api" + "github.com/open-sauced/pizza-cli/pkg/constants" "github.com/open-sauced/pizza-cli/pkg/utils" "github.com/spf13/cobra" ) @@ -72,7 +73,7 @@ func NewShowCommand() *cobra.Command { useBeta, _ := cmd.Flags().GetBool("beta") if useBeta { - endpoint = api.BetaAPIEndpoint + endpoint = constants.EndpointBeta } opts.APIClient = api.NewGoClient(endpoint) diff --git a/pkg/api/constants.go b/pkg/api/constants.go deleted file mode 100644 index 5f4a0d0..0000000 --- a/pkg/api/constants.go +++ /dev/null @@ -1,6 +0,0 @@ -package api - -const ( - APIEndpoint = "https://api.opensauced.pizza" - BetaAPIEndpoint = "https://beta.api.opensauced.pizza" -) diff --git a/pkg/utils/github.go b/pkg/utils/github.go index 68be868..05dbcb2 100644 --- a/pkg/utils/github.go +++ b/pkg/utils/github.go @@ -16,7 +16,7 @@ func GetOwnerAndRepoFromURL(input string) (owner, repo string, err error) { path := strings.Trim(u.Path, "/") parts := strings.Split(path, "/") if len(parts) != 2 { - return "", "", fmt.Errorf("Invalid URL: %s", input) + return "", "", fmt.Errorf("invalid URL: %s", input) } repoOwner = parts[0] repoName = parts[1] @@ -26,7 +26,7 @@ func GetOwnerAndRepoFromURL(input string) (owner, repo string, err error) { // check (owner/repo) format parts := strings.Split(input, "/") if len(parts) != 2 { - return "", "", fmt.Errorf("Invalid URL: %s", input) + return "", "", fmt.Errorf("invalid URL: %s", input) } repoOwner = parts[0] repoName = parts[1]