Repository: skanehira/github-tui Branch: main Commit: f56081431e3c Files: 39 Total size: 104.3 KB Directory structure: gitextract_8momnkvm/ ├── LICENSE ├── README.md ├── cmd/ │ └── ght/ │ └── main.go ├── config/ │ └── config.go ├── domain/ │ ├── assignees.go │ ├── comment.go │ ├── error.go │ ├── issue.go │ ├── item.go │ ├── label.go │ ├── milestone.go │ └── project.go ├── github/ │ ├── client.go │ ├── mutation_comment.go │ ├── mutation_issue.go │ ├── query.go │ ├── query_assignees.go │ ├── query_comment.go │ ├── query_issue.go │ ├── query_label.go │ ├── query_milestone.go │ ├── query_project.go │ └── query_repository.go ├── go.mod ├── go.sum ├── ui/ │ ├── assignees.go │ ├── comments.go │ ├── filter.go │ ├── issues.go │ ├── labels.go │ ├── milestones.go │ ├── projects.go │ ├── search.go │ ├── select.go │ ├── ui.go │ └── view.go └── utils/ ├── open.go ├── strings.go └── utils.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2020 skanehira Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # github-tui This is a TUI Client for GitHub. **Still Under Development** If you are using Vim, you can use [gh.vim](https://github.com/skanehira/gh.vim) instead. ![](https://i.gyazo.com/d7c8ca82e0aeb947f82c10b08d3eba35.png) ## Features ### Implemented - Issue - list - create - close - open - open browser - preview - edit - Issue comment - list - preview - delete - edit - add - quote reply ### Still Under Development - Issue - add assignees, labels, projects, milestone - remove assignees, labels, projects, milestone - PR - list - edit comment - add comment - delete comment - diff - create - close - change base - merge - Github Actions - re-run - list - log - File tree - preview - open browser - preview - Project - columns - open(if type is issue, pr) - add - remove - move - open browser - config - set default editor - set user keybindings ## Installation ```sh $ git clone https://github.com/skanehira/github-tui $ go install ./cmd/ght ``` ## Settings At first, please set personal access [token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) and email in config.yaml. ```yaml github: token: xxxxxxxxxxxxxxxxx ``` The config.yaml path must be in the bellow place. | OS | place | |------------|-----------------------------------------------------| | Window | `%AppData%¥ght¥config.yaml` | | Mac | `$HOME/Library/Application Support/ght/config.yaml` | | Linux/Unix | `$HOME/.config/ght/config.yaml` | ## Usage ```sh # current repository $ ght # specified repository $ ght owner/repo ``` ### Keybindings | UI | Keybinding | Description | |----------|----------------------|----------------------------------| | Common | `j`/`down arrow` | Move down by one row. | | Common | `k`/`up arrow` | Move up by one row. | | Common | `g`/`home` | Move to the top. | | Common | `G`/`end` | Move to the bottom. | | Common | `Ctrl-F`/`page down` | Move down by one page. | | Common | `Ctrl-B`/`page up` | Move up by one page. | | Common | `Ctrl-N` | Move next UI. | | Common | `Ctrl-P` | Move previous UI. | | Common | `Ctrl-C` | Finish app. | | Common | `Ctrl-G` | Focus to Issues | | Common | `Ctrl-T` | Focus to Filters | | Filters | `Enter` | Search with enter query. | | Issues | `h`/`left arrow` | Move left by one column. | | Issues | `l`/`right arrow` | Move right by one column. | | Issues | `Ctrl-J` | Check issue and move down. | | Issues | `Ctrl-K` | Check issue and move up. | | Issues | `e` | Edit and update issue body. | | Issues | `o` | Open checked issue. | | Issues | `c` | Close checked issue. | | Issues | `Ctrl-O` | Open checked issue on browser. | | Issues | `/` | filter with enter words | | Issues | `n` | Create new issue. | | Issues | `f` | Fetch more issue. | | Comments | `h`/`left arrow` | Move left by one column. | | Comments | `l`/`right arrow` | Move right by one column. | | Comments | `Ctrl-J` | Check comment and move down. | | Comments | `Ctrl-K` | Check comment and move up. | | Comments | `Ctrl-O` | Open checked comment on browser. | | Comments | `n` | Add new issue comment. | | Comments | `e` | Edit and update comment body. | | Comments | `r` | Quote reply comment. | | Comments | `/` | filter with enter words | | Preview | `/` | search with enter words | | Preview | `n` | move next word | | Preview | `N` | move previous word | | Preview | `o` | change to full screen | ### Note When you creating issue, you can specify multiple labels, projects and assignees with `,`. For instance, when you specify 2 labels then must input `label1,label2`. ![](https://i.gyazo.com/fb665369057c5f096517a24e606e7884.png) When you edit issue body with `Edit Body` button then `$EDITOR` be used. If `$EDITOR` is empty or not set, `vim` wll be used. ## Author skanehira ================================================ FILE: cmd/ght/main.go ================================================ package main import ( "flag" "fmt" "log" "os/exec" "strings" "github.com/skanehira/ght/config" "github.com/skanehira/ght/github" "github.com/skanehira/ght/ui" ) type Repo struct { Owner string Name string } func main() { config.Init() getRepoInfo() github.NewClient(config.GitHub.Token) if err := ui.New().Start(); err != nil { log.Fatal(err) } } func getRepoInfo() { flag.Parse() if len(flag.Args()) > 0 { args := strings.Split(flag.Arg(0), "/") if len(args) < 2 { log.Fatal("invalid args") } config.GitHub.Owner = args[0] config.GitHub.Repo = args[1] } else { repo, err := getOwnerRepo() if err != nil { log.Fatalf("invalid repo: %s", err) } config.GitHub.Owner = repo.Owner config.GitHub.Repo = repo.Name } } func getOwnerRepo() (*Repo, error) { if _, err := exec.LookPath("git"); err != nil { return nil, err } cmd := exec.Command("git", "remote", "get-url", "origin") out, err := cmd.CombinedOutput() result := strings.TrimRight(string(out), "\r\n") if err != nil { return nil, err } return parseRemote(result) } func parseRemote(remote string) (*Repo, error) { if strings.HasSuffix(remote, ".git") { remote = strings.TrimRight(remote, ".git") } var ownerRepo []string if strings.HasPrefix(remote, "ssh") { p := strings.Split(remote, "/") if len(p) < 1 { return nil, fmt.Errorf("cannot get owner/repo from remote: %s", remote) } ownerRepo = p[len(p)-2:] } else if strings.HasPrefix(remote, "git") { p := strings.Split(remote, ":") if len(p) < 1 { return nil, fmt.Errorf("cannot get owner/repo from remote: %s", remote) } ownerRepo = strings.Split(p[1], "/") } else if strings.HasPrefix(remote, "http") || strings.HasPrefix(remote, "https") { p := strings.Split(remote, "/") if len(p) < 1 { return nil, fmt.Errorf("cannot get owner/repo from remote: %s", remote) } ownerRepo = p[len(p)-2:] } repo := Repo{ Owner: ownerRepo[0], Name: ownerRepo[1], } return &repo, nil } ================================================ FILE: config/config.go ================================================ package config import ( "io" "log" "os" "path/filepath" "github.com/goccy/go-yaml" ) type github struct { Owner string Repo string Token string `yaml:"token"` } type app struct { File string `yaml:"file"` } const readThisMessage = "read this https://github.com/skanehira/github-tui?tab=readme-ov-file#settings to know more" var ( GitHub github App app ) func Init() { configDir, err := os.UserConfigDir() if err != nil { log.Fatal(err) } logFile := filepath.Join(configDir, "ght", "debug.log") output, err := os.Create(logFile) if err != nil { log.Fatal(err) } log.SetOutput(io.MultiWriter(output, os.Stderr)) configFile := filepath.Join(configDir, "ght", "config.yaml") b, err := os.ReadFile(configFile) if err != nil { if !os.IsNotExist(err) { log.Fatal(err) } log.Fatalf("Could not find configuration file, %s", readThisMessage) } var conf struct { GitHub github `yaml:"github"` } if err := yaml.Unmarshal(b, &conf); err != nil { log.Fatalf("cannot deserialize config file: %s", err) } if conf.GitHub.Token == "" { log.Fatalf("github token is empty, %s", readThisMessage) } App.File = configFile GitHub = conf.GitHub } ================================================ FILE: domain/assignees.go ================================================ package domain import "github.com/gdamore/tcell/v2" type AssignableUser struct { Login string } func (a *AssignableUser) Key() string { return a.Login } func (a *AssignableUser) Fields() []Field { return []Field{ {Text: a.Login, Color: tcell.ColorFuchsia}, } } ================================================ FILE: domain/comment.go ================================================ package domain import "github.com/gdamore/tcell/v2" type Comment struct { ID string Author string UpdatedAt string URL string Body string } func (c *Comment) Key() string { return c.ID } func (c *Comment) Fields() []Field { f := []Field{ {Text: c.Author, Color: tcell.ColorYellow}, {Text: c.UpdatedAt, Color: tcell.ColorWhite}, } return f } ================================================ FILE: domain/error.go ================================================ package domain import "errors" var ( ErrCommentBodyIsEmpty = errors.New("comment body is empty") ErrNotFoundComment = errors.New("not found comment") ErrNotFoundIssue = errors.New("not found issue") ) ================================================ FILE: domain/issue.go ================================================ package domain import ( "fmt" "github.com/gdamore/tcell/v2" ) type Issue struct { ID string Repo string RepoOwner string Number string State string Title string Body string Author string URL string Labels []Item Assignees []Item Comments []Item MileStone []Item Projects []Item } func (i *Issue) Key() string { return i.ID } func (i *Issue) Fields() []Field { stateColor := tcell.ColorGreen if i.State == "CLOSED" { stateColor = tcell.ColorRed } f := []Field{ {Text: fmt.Sprintf("%s/%s", i.RepoOwner, i.Repo), Color: tcell.ColorLightSalmon}, {Text: i.Number, Color: tcell.ColorBlue}, {Text: i.State, Color: stateColor}, {Text: i.Author, Color: tcell.ColorYellow}, {Text: i.Title, Color: tcell.ColorWhite}, } return f } ================================================ FILE: domain/item.go ================================================ package domain import "github.com/gdamore/tcell/v2" type Item interface { Key() string Fields() []Field } type Field struct { Text string Color tcell.Color } ================================================ FILE: domain/label.go ================================================ package domain import "github.com/gdamore/tcell/v2" type Label struct { Name string Description string } func (l *Label) Key() string { return l.Name } func (l *Label) Fields() []Field { return []Field{ {Text: l.Name, Color: tcell.ColorLightYellow}, } } ================================================ FILE: domain/milestone.go ================================================ package domain import "github.com/gdamore/tcell/v2" type Milestone struct { ID string Title string State string Description string URL string } func (m *Milestone) Key() string { return m.Title } func (m *Milestone) Fields() []Field { return []Field{ {Text: m.Title, Color: tcell.ColorGreen}, } } ================================================ FILE: domain/project.go ================================================ package domain import "github.com/gdamore/tcell/v2" type Project struct { Name string URL string } func (p *Project) Key() string { return p.Name } func (p *Project) Fields() []Field { return []Field{ {Text: p.Name, Color: tcell.ColorLightSalmon}, } } ================================================ FILE: github/client.go ================================================ package github import ( "context" "github.com/shurcooL/githubv4" "golang.org/x/oauth2" ) var client *githubv4.Client func NewClient(token string) { src := oauth2.StaticTokenSource( &oauth2.Token{AccessToken: token}, ) httpClient := oauth2.NewClient(context.Background(), src) client = githubv4.NewClient(httpClient) } func CreateIssue(input githubv4.CreateIssueInput) error { var m MutateCreateIssue return client.Mutate(context.Background(), &m, input, nil) } func GetRepos(variables map[string]interface{}) (*Repositories, error) { var q struct { RepositoryOwner struct { Repositories `graphql:"repositories(first: $first, after: $cursor, orderBy: {field: CREATED_AT, direction: DESC})"` } `graphql:"repositoryOwner(login: $login)"` } if err := client.Query(context.Background(), &q, variables); err != nil { return nil, err } return &q.RepositoryOwner.Repositories, nil } func GetRepo(variables map[string]interface{}) (*Repository, error) { var q struct { Repository `graphql:"repository(owner: $owner, name: $name)"` } if err := client.Query(context.Background(), &q, variables); err != nil { return nil, err } return &q.Repository, nil } func GetIssues(variables map[string]interface{}) (*Issues, error) { var q struct { Search Issues `graphql:"search(query: $query, type: ISSUE, first: $first, after: $cursor)"` } if err := client.Query(context.Background(), &q, variables); err != nil { return nil, err } issues := &Issues{ Nodes: q.Search.Nodes, PageInfo: q.Search.PageInfo, } return issues, nil } func GetIssue(variables map[string]interface{}) (*Issue, error) { var q struct { Repository struct { Issue *Issue `graphql:"issue(number: $number)"` } `graphql:"repository(owner: $owner, name: $name)"` } if err := client.Query(context.Background(), &q, variables); err != nil { return nil, err } return q.Repository.Issue, nil } func GetIssueTemplates(variables map[string]interface{}) ([]IssueTemplate, error) { var q struct { Repository struct { IssueTemplates []IssueTemplate } `graphql:"repository(name: $name, owner: $owner)"` } if err := client.Query(context.Background(), &q, variables); err != nil { return nil, err } return q.Repository.IssueTemplates, nil } func ReopenIssue(id string) error { input := githubv4.ReopenIssueInput{ IssueID: githubv4.String(id), } var m MutateOpenIsseue return client.Mutate(context.Background(), &m, input, nil) } func CloseIssue(id string) error { input := githubv4.CloseIssueInput{ IssueID: githubv4.String(id), } var m MutateCoseIssue return client.Mutate(context.Background(), &m, input, nil) } func GetRepoLabels(variables map[string]interface{}) (*Labels, error) { var q struct { Repository struct { Labels `graphql:"labels(first: $first, after: $cursor, orderBy: {field: CREATED_AT, direction: DESC})"` } `graphql:"repository(name: $name, owner: $owner)"` } if err := client.Query(context.Background(), &q, variables); err != nil { return nil, err } return &q.Repository.Labels, nil } func GetRepoMillestones(variables map[string]interface{}) (*Milestones, error) { var q struct { Repository struct { Milestones `graphql:"milestones(first: $first, after: $cursor, orderBy: {field: CREATED_AT, direction: DESC})"` } `graphql:"repository(name: $name, owner: $owner)"` } if err := client.Query(context.Background(), &q, variables); err != nil { return nil, err } return &q.Repository.Milestones, nil } func GetRepoProjects(variables map[string]interface{}) (*Projects, error) { var q struct { Repository struct { Projects `graphql:"projects(first: $first, after: $cursor, orderBy: {field: CREATED_AT, direction: DESC})"` } `graphql:"repository(name: $name, owner: $owner)"` } if err := client.Query(context.Background(), &q, variables); err != nil { return nil, err } return &q.Repository.Projects, nil } func GetRepoAssignableUsers(variables map[string]interface{}) (*AssignableUsers, error) { var q struct { Repository struct { AssignableUsers `graphql:"assignableUsers(first: $first, after: $cursor)"` } `graphql:"repository(name: $name, owner: $owner)"` } if err := client.Query(context.Background(), &q, variables); err != nil { return nil, err } return &q.Repository.AssignableUsers, nil } func DeleteIssueComment(id string) error { var m MutateDeleteComment input := githubv4.DeleteIssueCommentInput{ ID: githubv4.ID(id), } return client.Mutate(context.Background(), &m, input, nil) } func UpdateIssue(input githubv4.UpdateIssueInput) error { var m MutateUpdateIssue return client.Mutate(context.Background(), &m, input, nil) } func UpdateIssueComment(input githubv4.UpdateIssueCommentInput) error { var m MutateUpdateIssueComment return client.Mutate(context.Background(), &m, input, nil) } func AddIssueComment(input githubv4.AddCommentInput) error { var m MutateAddIssueComment return client.Mutate(context.Background(), &m, input, nil) } ================================================ FILE: github/mutation_comment.go ================================================ package github import "github.com/shurcooL/githubv4" type MutateDeleteComment struct { DeleteIssueComment struct { ClientMutationId githubv4.String } `graphql:"deleteIssueComment(input: $input)"` } type MutateUpdateIssueComment struct { UpdateIssueComment struct { ClientMutationId githubv4.String } `graphql:"updateIssueComment(input: $input)"` } ================================================ FILE: github/mutation_issue.go ================================================ package github import "github.com/shurcooL/githubv4" type MutateOpenIsseue struct { ReopenIssue struct { Issue struct { ID githubv4.String } } `graphql:"reopenIssue(input: $input)"` } type MutateCoseIssue struct { CloseIssue struct { Issue struct { ID githubv4.String } } `graphql:"closeIssue(input: $input)"` } type MutateCreateIssue struct { CreateIssue struct { Issue struct { ID githubv4.String } } `graphql:"createIssue(input: $input)"` } type MutateUpdateIssue struct { UpdateIssue struct { Issue struct { ID githubv4.ID } } `graphql:"updateIssue(input: $input)"` } type MutateAddIssueComment struct { AddIssueComment struct { ClientMutationID githubv4.String } `graphql:"addComment(input: $input)"` } ================================================ FILE: github/query.go ================================================ package github import "github.com/shurcooL/githubv4" type PageInfo struct { EndCursor githubv4.String HasNextPage githubv4.Boolean } ================================================ FILE: github/query_assignees.go ================================================ package github import ( "github.com/shurcooL/githubv4" "github.com/skanehira/ght/domain" ) type AssignableUser struct { Login githubv4.String } func (a *AssignableUser) ToDomain() *domain.AssignableUser { assignableUser := &domain.AssignableUser{ Login: string(a.Login), } return assignableUser } type AssignableUsers struct { Nodes []struct { ID githubv4.ID Login githubv4.String } PageInfo PageInfo } ================================================ FILE: github/query_comment.go ================================================ package github import ( "github.com/shurcooL/githubv4" "github.com/skanehira/ght/domain" ) type Comment struct { ID githubv4.String Author struct { Login githubv4.String } UpdatedAt githubv4.DateTime Body githubv4.String URL githubv4.URI } func (c *Comment) ToDomain() *domain.Comment { comment := &domain.Comment{ ID: string(c.ID), Author: string(c.Author.Login), UpdatedAt: c.UpdatedAt.Local().Format("2006/01/02 15:04:05"), URL: c.URL.String(), Body: string(c.Body), } return comment } ================================================ FILE: github/query_issue.go ================================================ package github import ( "reflect" "strconv" "github.com/shurcooL/githubv4" "github.com/skanehira/ght/domain" ) type Issue struct { ID githubv4.String Repository struct { ID githubv4.String Owner struct { Login githubv4.String } Name githubv4.String } Number githubv4.Int Body githubv4.String State githubv4.String Author struct { Login githubv4.String } Title githubv4.String URL githubv4.URI Labels Labels `graphql:"labels(first: 10)"` Assignees struct { Nodes []AssignableUser } `graphql:"assignees(first: 10)"` ProjectCards struct { Nodes []struct { Project Project } } `graphql:"projectCards(first: 10)"` Milestone Milestone Comments struct { Nodes []Comment } `graphql:"comments(first: 100)"` } func (i *Issue) ToDomain() *domain.Issue { issue := &domain.Issue{ ID: string(i.ID), Repo: string(i.Repository.Name), RepoOwner: string(i.Repository.Owner.Login), Number: strconv.Itoa(int(i.Number)), State: string(i.State), Author: string(i.Author.Login), URL: i.URL.String(), Title: string(i.Title), Body: string(i.Body), } labels := make([]domain.Item, len(i.Labels.Nodes)) for i, label := range i.Labels.Nodes { labels[i] = label.ToDomain() } issue.Labels = labels assignees := make([]domain.Item, len(i.Assignees.Nodes)) for i, a := range i.Assignees.Nodes { assignees[i] = a.ToDomain() } issue.Assignees = assignees comments := make([]domain.Item, len(i.Comments.Nodes)) for i, comment := range i.Comments.Nodes { comments[i] = comment.ToDomain() } issue.Comments = comments if !reflect.ValueOf(i.Milestone).IsZero() { issue.MileStone = append(issue.MileStone, i.Milestone.ToDomain()) } projects := make([]domain.Item, len(i.ProjectCards.Nodes)) for i, card := range i.ProjectCards.Nodes { projects[i] = card.Project.ToDomain() } issue.Projects = projects return issue } type Issues struct { Nodes []struct { Issue Issue `graphql:"... on Issue"` } PageInfo PageInfo } type IssueTemplate struct { About githubv4.String Body githubv4.String Name githubv4.String Title githubv4.String } ================================================ FILE: github/query_label.go ================================================ package github import ( "github.com/shurcooL/githubv4" "github.com/skanehira/ght/domain" ) type Label struct { ID githubv4.ID Name githubv4.String Description githubv4.String Color githubv4.String } func (l *Label) ToDomain() *domain.Label { label := &domain.Label{ Name: string(l.Name), Description: string(l.Description), } return label } type Labels struct { Nodes []Label PageInfo PageInfo } ================================================ FILE: github/query_milestone.go ================================================ package github import ( "github.com/shurcooL/githubv4" "github.com/skanehira/ght/domain" ) type Milestone struct { ID githubv4.ID Title githubv4.String State githubv4.String Description githubv4.String URL githubv4.URI } func (m *Milestone) ToDomain() *domain.Milestone { milestone := &domain.Milestone{ ID: m.ID.(string), Title: string(m.Title), State: string(m.State), Description: string(m.Description), URL: m.URL.String(), } return milestone } type Milestones struct { Nodes []Milestone PageInfo PageInfo } ================================================ FILE: github/query_project.go ================================================ package github import ( "github.com/shurcooL/githubv4" "github.com/skanehira/ght/domain" ) type Project struct { ID githubv4.ID Name githubv4.String URL githubv4.URI } func (p *Project) ToDomain() *domain.Project { project := &domain.Project{ Name: string(p.Name), URL: p.URL.String(), } return project } type Projects struct { Nodes []Project PageInfo PageInfo } ================================================ FILE: github/query_repository.go ================================================ package github import "github.com/shurcooL/githubv4" type Repository struct { ID githubv4.ID Name githubv4.String NameWithOwner githubv4.String CreatedAt githubv4.DateTime DefaultBranchRef struct { Name githubv4.String } Description githubv4.String LicenseInfo struct { Name githubv4.String } StargazerCount githubv4.Int URL githubv4.URI SSHURL githubv4.String } type Repositories struct { Nodes []Repository PageInfo PageInfo } ================================================ FILE: go.mod ================================================ module github.com/skanehira/ght go 1.15 require ( github.com/atotto/clipboard v0.1.2 github.com/davecgh/go-spew v1.1.1 // indirect github.com/gdamore/tcell/v2 v2.2.0 github.com/goccy/go-yaml v1.8.3 github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.6 // indirect github.com/rivo/tview v0.0.0-20210312174852-ae9464cc3598 github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a // indirect golang.org/x/net v0.0.0-20201026091529-146b70c837a4 // indirect golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 golang.org/x/sys v0.0.0-20210316092937-0b90fd5c4c48 // indirect ) ================================================ FILE: go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY= github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU= github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA= github.com/gdamore/tcell/v2 v2.1.0 h1:UnSmozHgBkQi2PGsFr+rpdXuAPRRucMegpQp3Z3kDro= github.com/gdamore/tcell/v2 v2.1.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA= github.com/gdamore/tcell/v2 v2.2.0 h1:vSyEgKwraXPSOkvCk7IwOSyX+Pv3V2cV9CikJMXg4U4= github.com/gdamore/tcell/v2 v2.2.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/goccy/go-yaml v1.8.3 h1:VGzw2KWSUyQX0yXai02S0nttBc+Oa4Kvh6RCFoxt8SE= github.com/goccy/go-yaml v1.8.3/go.mod h1:wS4gNoLalDSJxo/SpngzPQ2BN4uuZVLCmbM4S3vd4+Y= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 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-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rivo/tview v0.0.0-20210217110421-8a8f78a6dd01 h1:rtCzDXdaqhiRakJsz0bUj+3sOUjw82bJDcJrAzQ0u+M= github.com/rivo/tview v0.0.0-20210217110421-8a8f78a6dd01/go.mod h1:n2q/ydglZJ1kqxiNrnYO+FaX1H14vA0wKyIo953QakU= github.com/rivo/tview v0.0.0-20210312174852-ae9464cc3598 h1:AbRrGXhagPRDItERv7nauBUUPi7Ma3IGIj9FqkQKW6k= github.com/rivo/tview v0.0.0-20210312174852-ae9464cc3598/go.mod h1:VzCN9WX13RF88iH2CaGkmdHOlsy1ZZQcTmNwROqC+LI= 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/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b h1:0/ecDXh/HTHRtSDSFnD2/Ta1yQ5J76ZspVY4u0/jGFk= github.com/shurcooL/githubv4 v0.0.0-20200928013246-d292edc3691b/go.mod h1:hAF0iLZy4td2EX+/8Tw+4nodhlMrwN3HupfaXj3zkGo= github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a h1:KikTa6HtAK8cS1qjvUvvq4QO21QnwC+EfvB+OAuZ/ZU= github.com/shurcooL/graphql v0.0.0-20200928012149-18c5c3165e3a/go.mod h1:AuYgA5Kyo4c7HfUmvRGs/6rGlMMV/6B1bVnB9JxJEEg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201026091529-146b70c837a4 h1:awiuzyrRjJDb+OXi9ceHO3SDxVoN3JER57mhtqkdQBs= golang.org/x/net v0.0.0-20201026091529-146b70c837a4/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 h1:ld7aEMNHoBnnDAX15v1T6z31v8HwR2A9FYOuAhWqkwc= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210113181707-4bcb84eeeb78/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210218155724-8ebf48af031b h1:lAZ0/chPUDWwjqosYR0X4M490zQhMsiJ4K3DbA7o+3g= golang.org/x/sys v0.0.0-20210218155724-8ebf48af031b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210316092937-0b90fd5c4c48 h1:70qalHWW1n9yoI8B8zEQxFJO/D6NUWIX8SNmJO+rvNw= golang.org/x/sys v0.0.0-20210316092937-0b90fd5c4c48/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v9 v9.30.0 h1:Wk0Z37oBmKj9/n+tPyBHZmeL19LaCoK3Qq48VwYENss= gopkg.in/go-playground/validator.v9 v9.30.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= ================================================ FILE: ui/assignees.go ================================================ package ui import ( "github.com/gdamore/tcell/v2" ) var AssigneesUI *SelectUI func NewAssignableUI() { //getList := func(cursor *string) ([]List, github.PageInfo) { // v := map[string]interface{}{ // "owner": githubv4.String(config.GitHub.Owner), // "name": githubv4.String(config.GitHub.Repo), // "first": githubv4.Int(100), // "cursor": (*githubv4.String)(cursor), // } // resp, err := github.GetRepoAssignableUsers(v) // if err != nil { // return nil, github.PageInfo{} // } // assignees := make([]List, len(resp.Nodes)) // for i, p := range resp.Nodes { // assignees[i] = &AssignableUser{ // Login: string(p.Login), // } // } // return assignees, resp.PageInfo //} setOpt := func(ui *SelectUI) { ui.capture = func(event *tcell.EventKey) *tcell.EventKey { return event } } ui := NewSelectListUI(UIKindAssignee, tcell.ColorFuchsia, setOpt) AssigneesUI = ui } ================================================ FILE: ui/comments.go ================================================ package ui import ( "fmt" "log" "strconv" "strings" "github.com/gdamore/tcell/v2" "github.com/shurcooL/githubv4" "github.com/skanehira/ght/domain" "github.com/skanehira/ght/github" "github.com/skanehira/ght/utils" "golang.org/x/sync/errgroup" ) var CommentUI *SelectUI func NewCommentUI() { setOpt := func(ui *SelectUI) { ui.capture = func(event *tcell.EventKey) *tcell.EventKey { switch event.Rune() { case 'd': deleteComment() case 'n': item := IssueUI.GetSelect() if item == nil { return event } if err := createComment(item, ""); err != nil { UI.Message(err.Error(), func() { UI.app.SetFocus(CommentUI) }) } case 'e': if err := editComment(); err != nil { UI.Message(err.Error(), func() { UI.app.SetFocus(CommentUI) }) } case 'r': if err := quoteReply(); err != nil { UI.Message(err.Error(), func() { UI.app.SetFocus(CommentUI) }) } } switch event.Key() { case tcell.KeyCtrlO: for _, comment := range getSelectedComments() { if err := utils.Open(comment.URL); err != nil { log.Println(err) } } CommentUI.ClearSelected() CommentUI.UpdateView() } return event } ui.header = []string{ "", "Author", "UpdatedAt", } ui.hasHeader = len(ui.header) > 0 } ui := NewSelectListUI(UIKindComment, tcell.ColorYellow, setOpt) ui.SetSelectionChangedFunc(func(row, col int) { if row > 0 { CommentViewUI.updateView(ui.items[row-1].(*domain.Comment).Body) } }) CommentUI = ui } func quoteReply() error { item := CommentUI.GetSelect() if item == nil { return domain.ErrNotFoundComment } comment := item.(*domain.Comment) lines := strings.Split(comment.Body, "\n") for i := range lines { lines[i] = fmt.Sprintf("> %s", lines[i]) } body := strings.Join(lines, "\n") item = IssueUI.GetSelect() if item == nil { return domain.ErrNotFoundIssue } if err := createComment(item, body); err != nil { return err } return nil } func createComment(item domain.Item, body string) error { if err := editCommentBody(&body); err != nil { return err } input := githubv4.AddCommentInput{ SubjectID: githubv4.ID(item.Key()), Body: githubv4.String(body), } if err := github.AddIssueComment(input); err != nil { return err } if err := updateCommentUI(); err != nil { return err } return nil } func deleteComment() { UI.Confirm("Do you want to delete comments?", "Yes", func() error { comments := getSelectedComments() if len(comments) == 0 { return nil } var eg errgroup.Group for _, comment := range comments { id := comment.ID eg.Go(func() error { return github.DeleteIssueComment(id) }) } // When all the processing is completed this error be returned // because if some of delete action be success, need to update view deleteErr := eg.Wait() if deleteErr != nil { log.Println(deleteErr) } if err := updateCommentUI(); err != nil { return err } return deleteErr }, func() { UI.app.SetFocus(CommentUI) }) } func editComment() error { item := CommentUI.GetSelect() if item == nil { return domain.ErrNotFoundComment } comment := item.(*domain.Comment) oldBody := comment.Body if err := editCommentBody(&comment.Body); err != nil { return err } // if comment body is not changed, do nothing if oldBody == comment.Body { return nil } input := githubv4.UpdateIssueCommentInput{ ID: githubv4.ID(comment.ID), Body: githubv4.String(comment.Body), } if err := github.UpdateIssueComment(input); err != nil { return err } if err := updateCommentUI(); err != nil { return err } return nil } func editCommentBody(body *string) (err error) { UI.app.Suspend(func() { err = utils.Edit(body) }) if err != nil { return } if *body == "" { return domain.ErrCommentBodyIsEmpty } return } func getSelectedComments() []*domain.Comment { var comments []*domain.Comment if len(CommentUI.selected) == 0 { data := CommentUI.GetSelect() comments = append(comments, data.(*domain.Comment)) } else { for _, item := range CommentUI.selected { comments = append(comments, item.(*domain.Comment)) } } return comments } func updateCommentUI() error { item := IssueUI.GetSelect() if item == nil { return domain.ErrNotFoundIssue } oldIssue := item.(*domain.Issue) number, err := strconv.Atoi(oldIssue.Number) if err != nil { return err } m := map[string]interface{}{ "owner": githubv4.String(oldIssue.RepoOwner), "name": githubv4.String(oldIssue.Repo), "number": githubv4.Int(number), } issue, err := github.GetIssue(m) if err != nil { return err } newIssue := issue.ToDomain() IssueUI.UpdateItem(newIssue) if len(newIssue.Comments) > 0 { CommentUI.SetList(newIssue.Comments) CommentViewUI.updateView(newIssue.Comments[0].(*domain.Comment).Body) } else { CommentUI.ClearView() CommentViewUI.Clear() } return nil } ================================================ FILE: ui/filter.go ================================================ package ui import ( "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) var IssueFilterUI *FilterUI type ( SetFilterOpt func(ui *FilterUI) FilterUI struct { *tview.InputField } ) func NewFilterUI() { ui := &FilterUI{ InputField: tview.NewInputField().SetLabel("Filters").SetLabelWidth(8), } ui.SetBorderPadding(0, 0, 1, 0) ui.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyEnter: go IssueUI.GetList() } return event }) IssueFilterUI = ui } func (ui *FilterUI) SetQuery(query string) { ui.SetText(query) } func (ui *FilterUI) GetQuery() string { return ui.GetText() } func (ui *FilterUI) focus() { } func (ui *FilterUI) blur() { } ================================================ FILE: ui/issues.go ================================================ package ui import ( "fmt" "log" "strings" "sync" "time" "github.com/atotto/clipboard" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" "github.com/shurcooL/githubv4" "github.com/skanehira/ght/config" "github.com/skanehira/ght/domain" "github.com/skanehira/ght/github" "github.com/skanehira/ght/utils" ) var IssueUI *SelectUI func NewIssueUI() { opt := func(ui *SelectUI) { // initial query queries := []string{ fmt.Sprintf("repo:%s/%s", config.GitHub.Owner, config.GitHub.Repo), "state:open", } IssueFilterUI.SetQuery(strings.Join(queries, " ")) ui.getList = func(cursor *string) ([]domain.Item, *github.PageInfo) { var queries []string query := IssueFilterUI.GetQuery() if !strings.Contains(query, "is:issue") { queries = append(queries, "is:issue") } for _, q := range strings.Split(query, " ") { // execlude Pull request if strings.Contains(q, "type:pr") || strings.Contains(q, "is:pr") { continue } queries = append(queries, q) } query = strings.Join(queries, " ") IssueFilterUI.SetQuery(query) v := map[string]interface{}{ "query": githubv4.String(query), "first": githubv4.Int(30), "cursor": (*githubv4.String)(cursor), } resp, err := github.GetIssues(v) if err != nil { log.Println(err) return nil, nil } issues := make([]domain.Item, len(resp.Nodes)) for i, node := range resp.Nodes { issues[i] = node.Issue.ToDomain() } return issues, &resp.PageInfo } ui.capture = func(event *tcell.EventKey) *tcell.EventKey { switch event.Rune() { case 'y': yankIssueURLs() case 'o': go openIssues() case 'c': go closeIssues() case 'n': createIssueForm() case 'e': editIssue() } switch event.Key() { case tcell.KeyCtrlO: openBrowser() } return event } ui.header = []string{ "", "Repo", "Number", "State", "Author", "Title", } ui.hasHeader = len(ui.header) > 0 } ui := NewSelectListUI(UIKindIssue, tcell.ColorBlue, opt) ui.SetSelectionChangedFunc(func(row, col int) { updateUIRelatedIssue(ui, row) }) IssueUI = ui } func getSelectedIssues() []*domain.Issue { var issues []*domain.Issue if len(IssueUI.selected) == 0 { data := IssueUI.GetSelect() if data != nil { issues = append(issues, data.(*domain.Issue)) } } else { for _, item := range IssueUI.selected { issues = append(issues, item.(*domain.Issue)) } } return issues } func yankIssueURLs() { var urls []string for _, issue := range getSelectedIssues() { urls = append(urls, issue.URL) } url := strings.Join(urls, "\n") if err := clipboard.WriteAll(url); err != nil { log.Println(err) } IssueUI.ClearSelected() IssueUI.UpdateView() } func openIssues() { var wg sync.WaitGroup for _, issue := range getSelectedIssues() { wg.Add(1) go func(issue *domain.Issue) { defer wg.Done() if err := github.ReopenIssue(issue.ID); err != nil { log.Println(err) return } issue.State = "OPEN" }(issue) } wg.Wait() IssueUI.ClearSelected() IssueUI.UpdateView() } func closeIssues() { var wg sync.WaitGroup for _, issue := range getSelectedIssues() { wg.Add(1) go func(issue *domain.Issue) { defer wg.Done() if err := github.CloseIssue(issue.ID); err != nil { log.Println(err) return } issue.State = "CLOSED" }(issue) } wg.Wait() IssueUI.ClearSelected() IssueUI.UpdateView() } func openBrowser() { for _, issue := range getSelectedIssues() { if err := utils.Open(issue.URL); err != nil { log.Println(err) } } IssueUI.ClearSelected() IssueUI.UpdateView() } func createIssueForm() { // repo var repo string input := IssueFilterUI.GetQuery() for _, word := range strings.Split(input, " ") { if strings.Contains(word, "repo:") { repo = strings.TrimPrefix(word, "repo:") break } } if repo == "" { return } form := tview.NewForm() form.SetBorder(true) form.SetTitle("New issue") form.SetTitleAlign(tview.AlignLeft) inputWidth := 70 repoInput := tview.NewInputField().SetLabel("Repository"). SetText(repo).SetLabelWidth(inputWidth). SetAcceptanceFunc(func(textToCheck string, lastChar rune) bool { return false }) repoInput.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyCtrlN { form.SetFocus(2) } return event }) form.AddFormItem(repoInput) s := strings.Split(repoInput.GetText(), "/") owner := s[0] name := s[1] var repoID githubv4.ID resp, err := github.GetRepo(map[string]interface{}{ "owner": githubv4.String(owner), "name": githubv4.String(name), }) if err != nil { UI.Message(err.Error(), func() { UI.app.SetFocus(IssueUI) }) return } repoID = resp.ID form.SetFocus(1) // autocomplete for assignees, labels, projects, milestones autocompleteFunc := func(text string, items []string) []string { if text == "" { return nil } words := strings.Split(text, ",") word := words[len(words)-1] var results []string for _, l := range items { var isDuplicate bool for _, w := range words[:len(words)-1] { if w == l { isDuplicate = true break } } if isDuplicate { continue } if strings.Contains(strings.ToLower(l), strings.ToLower(word)) { words = append(words[:len(words)-1], l) results = append(results, strings.Join(words, ",")) } } return results } // graphql query variables v := map[string]interface{}{ "owner": githubv4.String(owner), "name": githubv4.String(name), "first": githubv4.Int(100), "cursor": (*githubv4.String)(nil), } // title titleInput := tview.NewInputField().SetLabel("Title").SetLabelWidth(inputWidth) form.AddFormItem(titleInput) // assignees assigneesInput := tview.NewInputField().SetLabel("Assignees").SetLabelWidth(inputWidth) form.AddFormItem(assigneesInput) userMap := map[string]githubv4.ID{} go func() { resp, err := github.GetRepoAssignableUsers(v) if err != nil { log.Println(err) return } if len(resp.Nodes) == 0 { UI.app.QueueUpdateDraw(func() { form.RemoveFormItem(2) }) return } var users []string for _, u := range resp.Nodes { name := string(u.Login) userMap[name] = u.ID users = append(users, name) } assigneesInput.SetAutocompleteFunc(func(text string) []string { return autocompleteFunc(text, users) }) }() // labels labelInput := tview.NewInputField().SetLabel("Labels").SetLabelWidth(inputWidth) labelMap := map[string]githubv4.ID{} form.AddFormItem(labelInput) go func() { resp, err := github.GetRepoLabels(v) if err != nil { log.Println(err) return } if len(resp.Nodes) == 0 { UI.app.QueueUpdateDraw(func() { form.RemoveFormItem(3) }) return } var labels []string for _, l := range resp.Nodes { name := string(l.Name) labelMap[name] = l.ID labels = append(labels, name) } labelInput.SetAutocompleteFunc(func(text string) []string { return autocompleteFunc(text, labels) }) }() // projects projectInput := tview.NewInputField().SetLabel("Projects").SetLabelWidth(inputWidth) projectMap := map[string]githubv4.ID{} form.AddFormItem(projectInput) go func() { resp, err := github.GetRepoProjects(v) if err != nil { log.Println(err) return } if len(resp.Nodes) == 0 { UI.app.QueueUpdateDraw(func() { form.RemoveFormItem(4) }) return } var projects []string for _, project := range resp.Nodes { name := string(project.Name) projectMap[name] = project.ID projects = append(projects, name) } projectInput.SetAutocompleteFunc(func(text string) []string { return autocompleteFunc(text, projects) }) }() // milestones milestoneDropDown := tview.NewDropDown().SetLabel("MileStone").SetLabelWidth(inputWidth) var milestoneID *githubv4.ID form.AddFormItem(milestoneDropDown) go func() { resp, err := github.GetRepoMillestones(v) if err != nil { log.Println(err) return } if len(resp.Nodes) == 0 { UI.app.QueueUpdateDraw(func() { form.RemoveFormItem(5) }) return } milestones := map[string]*githubv4.ID{} var titles []string for _, milestone := range resp.Nodes { title := string(milestone.Title) milestones[title] = &milestone.ID titles = append(titles, title) } milestoneDropDown.SetOptions(titles, func(text string, index int) { milestoneID = milestones[text] }) }() var issueBody string templateDropDown := tview.NewDropDown().SetLabel("Template").SetLabelWidth(inputWidth) go func() { v := map[string]interface{}{ "owner": githubv4.String(owner), "name": githubv4.String(name), } resp, err := github.GetIssueTemplates(v) if err != nil { log.Println(err) return } if len(resp) == 0 { return } issueTemplates := map[string]string{} var names []string for _, te := range resp { issueTemplates[string(te.Name)] = string(te.Body) names = append(names, string(te.Name)) } templateDropDown.SetOptions(names, func(text string, index int) { issueBody = issueTemplates[text] }) UI.app.QueueUpdateDraw(func() { form.AddFormItem(templateDropDown) }) }() form.AddButton("Edit Body", func() { UI.app.Suspend(func() { if err := utils.Edit(&issueBody); err != nil { log.Println(err) return } }) }) form.AddButton("Create", func() { input := githubv4.CreateIssueInput{ Title: githubv4.String(titleInput.GetText()), RepositoryID: repoID, } if milestoneID != nil { input.MilestoneID = milestoneID } // get assignee users var userIDs []githubv4.ID if text := assigneesInput.GetText(); text != "" { for _, name := range strings.Split(text, ",") { if name == "" { continue } userIDs = append(userIDs, userMap[name]) } input.AssigneeIDs = &userIDs } // get labels var labelIDs []githubv4.ID if text := labelInput.GetText(); text != "" { for _, name := range strings.Split(text, ",") { if name == "" { continue } labelIDs = append(labelIDs, labelMap[name]) } input.LabelIDs = &labelIDs } // get projects var projectIDs []githubv4.ID if text := projectInput.GetText(); text != "" { for _, name := range strings.Split(text, ",") { if name == "" { continue } projectIDs = append(projectIDs, projectMap[name]) } input.ProjectIDs = &projectIDs } body := githubv4.String(issueBody) input.Body = &body if err := github.CreateIssue(input); err != nil { UI.Message(err.Error(), func() { UI.pages.SwitchToPage("form").ShowPage("main") }) } else { UI.pages.RemovePage("form").ShowPage("main") UI.app.SetFocus(IssueUI) go func() { time.Sleep(1 * time.Second) IssueUI.GetList() }() } }) form.AddButton("Cancel", func() { UI.pages.RemovePage("form").ShowPage("main") UI.app.SetFocus(IssueUI) }) form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyCtrlN: k := tcell.NewEventKey(tcell.KeyTab, 0, tcell.ModNone) UI.app.QueueEvent(k) case tcell.KeyCtrlP: k := tcell.NewEventKey(tcell.KeyBacktab, 0, tcell.ModNone) UI.app.QueueEvent(k) } return event }) UI.pages.AddAndSwitchToPage("form", UI.Modal(form, 100, 19), true).ShowPage("main") } func editIssue() { item := IssueUI.GetSelect() if item == nil { return } issue := item.(*domain.Issue) focus := func() { UI.app.SetFocus(IssueUI) } var err error old := issue.Body UI.app.Suspend(func() { err = utils.Edit(&issue.Body) }) if err != nil { UI.Message(err.Error(), focus) return } // if issue body not edited do nothing if old == issue.Body { return } input := githubv4.UpdateIssueInput{ ID: githubv4.ID(issue.ID), Body: githubv4.NewString(githubv4.String(issue.Body)), } if err := github.UpdateIssue(input); err != nil { UI.Message(err.Error(), focus) return } IssueViewUI.updateView(issue.Body) } func updateUIRelatedIssue(ui *SelectUI, row int) { if row > 0 && row <= len(ui.items) { issue := ui.items[row-1].(*domain.Issue) IssueViewUI.updateView(issue.Body) if len(issue.Comments) > 0 { CommentUI.SetList(issue.Comments) CommentViewUI.updateView(issue.Comments[0].(*domain.Comment).Body) } else { CommentUI.ClearView() CommentViewUI.Clear() } if len(issue.Assignees) > 0 { AssigneesUI.SetList(issue.Assignees) } else { AssigneesUI.ClearView() } if len(issue.Labels) > 0 { LabelUI.SetList(issue.Labels) } else { LabelUI.ClearView() } if len(issue.MileStone) > 0 { MilestoneUI.SetList(issue.MileStone) } else { MilestoneUI.ClearView() } if len(issue.Projects) > 0 { ProjectUI.SetList(issue.Projects) } else { ProjectUI.ClearView() } } } ================================================ FILE: ui/labels.go ================================================ package ui import ( "github.com/gdamore/tcell/v2" ) var LabelUI *SelectUI func NewLabelsUI() { //getList := func(cursor *string) ([]List, github.PageInfo) { // v := map[string]interface{}{ // "owner": githubv4.String(config.GitHub.Owner), // "name": githubv4.String(config.GitHub.Repo), // "first": githubv4.Int(100), // "cursor": (*githubv4.String)(cursor), // } // resp, err := github.GetRepoLabels(v) // if err != nil { // log.Println(err) // return nil, github.PageInfo{} // } // labels := make([]List, len(resp.Nodes)) // for i, l := range resp.Nodes { // name := string(l.Name) // description := string(l.Description) // labels[i] = &Label{ // Name: name, // Description: description, // } // } // return labels, resp.PageInfo //} setOpt := func(ui *SelectUI) { ui.capture = func(event *tcell.EventKey) *tcell.EventKey { return event } } ui := NewSelectListUI(UIKindLabel, tcell.ColorLightYellow, setOpt) LabelUI = ui } ================================================ FILE: ui/milestones.go ================================================ package ui import ( "log" "github.com/gdamore/tcell/v2" "github.com/skanehira/ght/domain" "github.com/skanehira/ght/utils" ) var MilestoneUI *SelectUI func NewMilestoneUI() { //getList := func(cursor *string) ([]List, github.PageInfo) { // v := map[string]interface{}{ // "owner": githubv4.String(config.GitHub.Owner), // "name": githubv4.String(config.GitHub.Repo), // "first": githubv4.Int(100), // "cursor": (*githubv4.String)(cursor), // } // resp, err := github.GetRepoMillestones(v) // if err != nil { // return nil, github.PageInfo{} // } // milestones := make([]List, len(resp.Nodes)) // for i, m := range resp.Nodes { // milestones[i] = &Milestone{ // Title: string(m.Title), // } // } // return milestones, resp.PageInfo //} setOpt := func(ui *SelectUI) { ui.capture = func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyCtrlO: var urls []string if len(MilestoneUI.selected) == 0 { data := MilestoneUI.GetSelect() if data != nil { urls = append(urls, data.(*domain.Milestone).URL) } } else { for _, s := range MilestoneUI.selected { urls = append(urls, s.(*domain.Milestone).URL) } } for _, url := range urls { if err := utils.Open(url); err != nil { log.Println(err) } } } return event } } ui := NewSelectListUI(UIKindMilestones, tcell.ColorGreen, setOpt) MilestoneUI = ui } ================================================ FILE: ui/projects.go ================================================ package ui import ( "log" "github.com/gdamore/tcell/v2" "github.com/skanehira/ght/domain" "github.com/skanehira/ght/utils" ) var ProjectUI *SelectUI func NewProjectUI() { //getList := func(cursor *string) ([]List, github.PageInfo) { // v := map[string]interface{}{ // "owner": githubv4.String(config.GitHub.Owner), // "name": githubv4.String(config.GitHub.Repo), // "first": githubv4.Int(100), // "cursor": (*githubv4.String)(cursor), // } // resp, err := github.GetRepoProjects(v) // if err != nil { // return nil, github.PageInfo{} // } // projects := make([]List, len(resp.Nodes)) // for i, m := range resp.Nodes { // projects[i] = &Project{ // Name: string(m.Name), // } // } // return projects, resp.PageInfo //} setOpt := func(ui *SelectUI) { ui.capture = func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyCtrlO: var urls []string if len(ProjectUI.selected) == 0 { data := ProjectUI.GetSelect() if data != nil { urls = append(urls, data.(*domain.Project).URL) } } else { for _, s := range ProjectUI.selected { urls = append(urls, s.(*domain.Project).URL) } } for _, url := range urls { if err := utils.Open(url); err != nil { log.Println(err) } } } return event } } ui := NewSelectListUI(UIKindProject, tcell.ColorLightSalmon, setOpt) ProjectUI = ui } ================================================ FILE: ui/search.go ================================================ package ui import ( "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) var SearchUI *searchUI type SearchFunc func(text string) type searchUI struct { *tview.InputField SearchFunc SearchFunc FocusFunc func() } func NewSearchUI() { ui := &searchUI{ InputField: tview.NewInputField(), SearchFunc: func(text string) {}, FocusFunc: func() {}, } ui.SetDoneFunc(func(key tcell.Key) { ui.FocusFunc() }) SearchUI = ui } func (s *searchUI) SetSerachFunc(f SearchFunc) { s.SetChangedFunc(f) } func (s *searchUI) SetFocusFunc(f func()) { s.FocusFunc = f } func (s *searchUI) focus() { } func (s *searchUI) blur() { } ================================================ FILE: ui/select.go ================================================ package ui import ( "strings" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" "github.com/skanehira/ght/domain" "github.com/skanehira/ght/github" ) const ( unselected = "\u25ef" selected = "\u25c9" ) type UIKind string const ( UIKindIssue UIKind = "issues" UIKindAssignee = "assignees" UIKindComment = "comments" UIKindLabel = "labels" UIKindMilestones = "milestones" UIKindProject = "projects" UIKindIssueView = "issue preview" UIKindCommentView = "comment preview" UIKindCommonView = "preview" ) type ( SetSelectUIOpt func(ui *SelectUI) GetListFunc func(cursor *string) ([]domain.Item, *github.PageInfo) CaptureFunc func(event *tcell.EventKey) *tcell.EventKey ) type SelectUI struct { uiKind UIKind cursor *string hasNext bool getList GetListFunc capture CaptureFunc header []string hasHeader bool originItems []domain.Item items []domain.Item selected map[string]domain.Item boxColor tcell.Color searchWord string *tview.Table } func NewSelectListUI(uiKind UIKind, boxColor tcell.Color, setOpt SetSelectUIOpt) *SelectUI { ui := &SelectUI{ uiKind: uiKind, hasNext: true, selected: make(map[string]domain.Item), boxColor: boxColor, Table: tview.NewTable().SetSelectable(false, false), } ui.SetBorder(true).SetTitle(string(uiKind)).SetTitleAlign(tview.AlignLeft) ui.SetBorderColor(boxColor) setOpt(ui) go ui.Init() return ui } func (ui *SelectUI) GetList() { if ui.getList != nil { list, pageInfo := ui.getList(nil) if pageInfo != nil { ui.hasNext = bool(pageInfo.HasNextPage) cursor := string(pageInfo.EndCursor) ui.originItems = list ui.cursor = &cursor ui.Select(0, 0) ui.UpdateView() } } } func (ui *SelectUI) SetList(list []domain.Item) { ui.originItems = list ui.selected = make(map[string]domain.Item) ui.Select(0, 0) ui.UpdateView() } func (ui *SelectUI) FetchList() { if ui.hasNext && ui.getList != nil { list, pageInfo := ui.getList(ui.cursor) ui.hasNext = bool(pageInfo.HasNextPage) cursor := string(pageInfo.EndCursor) ui.originItems = append(ui.originItems, list...) ui.cursor = &cursor ui.UpdateView() } } func (ui *SelectUI) UpdateView() { UI.updater <- func() { ui.Clear() for i, h := range ui.header { ui.SetCell(0, i, &tview.TableCell{ Text: h, NotSelectable: true, Align: tview.AlignLeft, Color: tcell.ColorWhite, BackgroundColor: tcell.ColorDefault, Attributes: tcell.AttrBold | tcell.AttrUnderline, }) } if len(ui.originItems) < 1 { return } h := 0 if ui.hasHeader { h++ ui.SetFixed(1, 0) } selectColor := ui.originItems[0].Fields()[0].Color ui.items = []domain.Item{} if ui.searchWord != "" { for _, data := range ui.originItems { for _, f := range data.Fields() { if strings.Contains(strings.ToLower(f.Text), strings.ToLower(ui.searchWord)) { ui.items = append(ui.items, data) break } } } } else { ui.items = ui.originItems } for i, data := range ui.items { if _, ok := ui.selected[data.Key()]; ok { ui.SetCell(i+h, 0, tview.NewTableCell(selected).SetTextColor(selectColor)) } else { ui.SetCell(i+h, 0, tview.NewTableCell(unselected).SetTextColor(selectColor)) } for j, f := range data.Fields() { ui.SetCell(i+h, j+1, tview.NewTableCell(f.Text).SetTextColor(f.Color)) } } ui.ScrollToBeginning() // when update filter, then update ui related issue primitives if ui.uiKind == UIKindIssue { row, _ := ui.GetSelection() if row == 0 { row = 1 } updateUIRelatedIssue(ui, row) } } } func (ui *SelectUI) Init() { ui.GetList() searchFunc := func(text string) { ui.searchWord = text ui.UpdateView() } ui.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyCtrlJ: row, col := ui.GetSelection() max := len(ui.items) if ui.hasHeader { max++ } if row < max { ui.toggleSelected(row) } if row+1 < max { ui.Select(row+1, col) } case tcell.KeyCtrlK: row, col := ui.GetSelection() min := 0 if ui.hasHeader { min++ } if row > min { ui.toggleSelected(row - 1) } if row > min { ui.Select(row-1, col) } } switch event.Rune() { case 'f': go ui.FetchList() case '/': SearchUI.SetSerachFunc(searchFunc) SearchUI.SetFocusFunc(func() { UI.app.SetFocus(ui) }) UI.app.SetFocus(SearchUI) } return ui.capture(event) }) } func (ui *SelectUI) toggleSelected(row int) { var data domain.Item if ui.hasHeader { data = ui.items[row-1] } else { data = ui.items[row] } selectColor := ui.items[0].Fields()[0].Color if _, ok := ui.selected[data.Key()]; ok { delete(ui.selected, data.Key()) ui.SetCell(row, 0, tview.NewTableCell(unselected).SetTextColor(selectColor)) } else { ui.selected[data.Key()] = data ui.SetCell(row, 0, tview.NewTableCell(selected).SetTextColor(selectColor)) } } func (ui *SelectUI) UpdateItem(item domain.Item) { for i, t := range ui.originItems { if t.Key() == item.Key() { ui.originItems[i] = item } } } func (ui *SelectUI) GetSelect() domain.Item { row, _ := ui.GetSelection() if ui.hasHeader { row = row - 1 } if len(ui.items) > row { id := ui.items[row].Key() for _, item := range ui.originItems { if item.Key() == id { return item } } } return nil } func (ui *SelectUI) focus() { ui.SetSelectable(true, false) } func (ui *SelectUI) blur() { ui.SetSelectable(false, false) } func (ui *SelectUI) ClearView() { ui.Clear() ui.ClearSelected() } func (ui *SelectUI) ClearSelected() { ui.selected = make(map[string]domain.Item) } ================================================ FILE: ui/ui.go ================================================ package ui import ( "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) var ( UI *ui ) type Primitive interface { focus() blur() tview.Primitive } type ui struct { app *tview.Application pages *tview.Pages current int primitives []Primitive primitiveLen int updater chan func() } func New() *ui { ui := &ui{ app: tview.NewApplication(), } ui.updater = make(chan func(), 100) UI = ui return ui } func (ui *ui) canFocus() bool { fs := ui.app.GetFocus() if fs == nil { return false } switch fs.(type) { case *FilterUI, *SelectUI, *ViewUI: return true } return false } func (ui *ui) toNextUI() { if !ui.canFocus() { return } ui.primitives[ui.current].blur() if ui.primitiveLen-1 > ui.current { ui.current++ } else { ui.current = 0 } p := ui.primitives[ui.current] p.focus() ui.app.SetFocus(p) } func (ui *ui) toPrevUI() { if !ui.canFocus() { return } ui.primitives[ui.current].blur() if ui.current == 0 { ui.current = ui.primitiveLen - 1 } else { ui.current-- } p := ui.primitives[ui.current] p.focus() ui.app.SetFocus(p) } func (ui *ui) Modal(p tview.Primitive, width, height int) tview.Primitive { return tview.NewGrid(). SetColumns(0, width, 0). SetRows(0, height, 0). AddItem(p, 1, 1, 1, 1, 0, 0, true) } func (ui *ui) Message(msg string, focusFunc func()) { modal := tview.NewModal(). SetText(msg). AddButtons([]string{"OK"}). SetDoneFunc(func(_ int, _ string) { ui.pages.RemovePage("message").ShowPage("main") focusFunc() }) ui.pages.AddAndSwitchToPage("message", ui.Modal(modal, 80, 29), true).ShowPage("main") } func (ui *ui) FullScreenPreview(contents string, focus func()) { CommonViewUI.SetText(contents).ScrollToBeginning() CommonViewUI.setFocus = focus grid := tview.NewGrid().SetRows(0, 1). AddItem(CommonViewUI, 0, 0, 1, 1, 0, 0, true) ui.pages.AddAndSwitchToPage("fullScreenPreview", grid, true).ShowPage("main") } func (ui *ui) Confirm(msg, doLabel string, doFunc func() error, focusFunc func()) { modal := tview.NewModal(). SetText(msg). AddButtons([]string{doLabel, "Cancel"}). SetDoneFunc(func(_ int, buttonLabel string) { ui.pages.RemovePage("modal").ShowPage("main") focusFunc() if buttonLabel == doLabel { if err := doFunc(); err != nil { ui.Message(err.Error(), func() { focusFunc() }) } } }) ui.pages.AddAndSwitchToPage("modal", ui.Modal(modal, 80, 29), true).ShowPage("main") } func (ui *ui) Start() error { NewFilterUI() NewViewUI(UIKindIssueView) NewViewUI(UIKindCommentView) NewViewUI(UIKindCommonView) NewIssueUI() NewLabelsUI() NewMilestoneUI() NewProjectUI() NewAssignableUI() NewCommentUI() NewSearchUI() ui.primitives = []Primitive{IssueFilterUI, AssigneesUI, LabelUI, MilestoneUI, ProjectUI, IssueUI, IssueViewUI, CommentUI, CommentViewUI} ui.primitiveLen = len(ui.primitives) // for readability row, col, rowSpan, colSpan := 0, 0, 0, 0 grid := tview.NewGrid().SetRows(1, 0, 0, 0, 0, 0, 0, 0, 0, 1). AddItem(IssueFilterUI, row, col, rowSpan+1, colSpan+3, 0, 0, true). AddItem(IssueUI, row+1, col+1, rowSpan+4, colSpan+3, 0, 0, true). AddItem(AssigneesUI, row+1, col, rowSpan+1, colSpan+1, 0, 0, true). AddItem(LabelUI, row+2, col, rowSpan+1, colSpan+1, 0, 0, true). AddItem(MilestoneUI, row+3, col, rowSpan+1, colSpan+1, 0, 0, true). AddItem(ProjectUI, row+4, col, rowSpan+1, colSpan+1, 0, 0, true). AddItem(CommentUI, row+5, col, rowSpan+4, colSpan+4, 0, 0, true). AddItem(IssueViewUI, row+1, col+4, rowSpan+4, colSpan+3, 0, 0, true). AddItem(CommentViewUI, row+5, col+4, rowSpan+4, colSpan+3, 0, 0, true). AddItem(SearchUI, row+9, col, rowSpan+1, colSpan+7, 0, 0, true) ui.pages = tview.NewPages(). AddAndSwitchToPage("main", grid, true) ui.app.SetRoot(ui.pages, true) ui.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyCtrlN: UI.toNextUI() case tcell.KeyCtrlP: UI.toPrevUI() case tcell.KeyCtrlG: ui.primitives[ui.current].blur() ui.current = 5 p := ui.primitives[ui.current] p.focus() ui.app.SetFocus(IssueUI) case tcell.KeyCtrlT: ui.primitives[ui.current].blur() ui.current = 0 p := ui.primitives[ui.current] p.focus() ui.app.SetFocus(IssueFilterUI) } return event }) ui.current = 5 ui.app.SetFocus(IssueUI) IssueUI.focus() go func() { for f := range UI.updater { go ui.app.QueueUpdateDraw(f) } }() if err := ui.app.Run(); err != nil { ui.app.Stop() return err } return nil } ================================================ FILE: ui/view.go ================================================ package ui import ( "strconv" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" "github.com/skanehira/ght/utils" ) var ( IssueViewUI *ViewUI CommentViewUI *ViewUI CommonViewUI *ViewUI ) type ViewUI struct { *tview.TextView regionIndex int regionLength int regionIDs []string uiKind UIKind setFocus func() } func NewViewUI(uiKind UIKind) { ui := &ViewUI{ TextView: tview.NewTextView(), uiKind: uiKind, } ui.SetBorder(true).SetTitle(string(uiKind)).SetTitleAlign(tview.AlignLeft) ui.SetDynamicColors(true).SetWordWrap(false).SetRegions(true) ui.SetBorderPadding(1, 1, 1, 1) var setFocus func() switch uiKind { case UIKindIssueView: IssueViewUI = ui setFocus = func() { UI.app.SetFocus(IssueViewUI) } case UIKindCommentView: CommentViewUI = ui setFocus = func() { UI.app.SetFocus(CommentViewUI) } case UIKindCommonView: CommonViewUI = ui } searchFunc := func(input string) { text := ui.GetText(true) if input != "" { ui.regionIDs, text = utils.Replace(text, input, `[#ff0000]["%d"]`+input+`[""][white]`, -1) ui.regionLength = len(ui.regionIDs) if ui.regionLength > 0 { ui.regionIndex = 0 ui.Highlight(ui.regionIDs[0]).ScrollToHighlight() } } go ui.updateView(text) } ui.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Rune() { case '/': SearchUI.SetText("") SearchUI.SetSerachFunc(searchFunc) SearchUI.SetFocusFunc(func() { UI.app.SetFocus(ui) }) UI.app.SetFocus(SearchUI) case 'n': if ui.regionLength > 0 { ui.regionIndex = (ui.regionIndex + 1) % ui.regionLength ui.Highlight(strconv.Itoa(ui.regionIndex)).ScrollToHighlight() } case 'N': if ui.regionLength > 0 { ui.regionIndex = (ui.regionIndex - 1 + ui.regionLength) % ui.regionLength ui.Highlight(strconv.Itoa(ui.regionIndex)).ScrollToHighlight() } case 'o': if ui.uiKind == UIKindCommonView { UI.pages.SwitchToPage("main") ui.setFocus() return event } UI.FullScreenPreview(ui.GetText(true), setFocus) } //switch event.Key() { //} return event }) } func (ui *ViewUI) updateView(text string) { UI.updater <- func() { ui.SetText(text).ScrollToBeginning() //out, err := glamour.Render(text, "dark") //if err != nil { // out = err.Error() //} //ui.SetText(tview.TranslateANSI(out)).ScrollToBeginning() } } func (v *ViewUI) focus() {} func (v *ViewUI) blur() {} ================================================ FILE: utils/open.go ================================================ package utils import ( "errors" "os/exec" "runtime" "strings" ) func Open(url string) error { args := []string{} switch runtime.GOOS { case "windows": r := strings.NewReplacer("&", "^&") args = []string{"cmd", "start", "/", r.Replace(url)} case "linux": args = []string{"xdg-open", url} case "darwin": args = []string{"open", url} } out, err := exec.Command(args[0], args[1:]...).CombinedOutput() if err != nil { return errors.New(string(out)) } return nil } ================================================ FILE: utils/strings.go ================================================ package utils import ( "fmt" "strconv" "strings" "unicode/utf8" ) // Replace is customized for this project // https://github.com/golang/go/blob/a8942d2cffd80c68febe1c908a0eb464d2f5bb40/src/strings/strings.go#L924 func Replace(s, old, new string, n int) ([]string, string) { if old == new || n == 0 { return nil, s // avoid allocation } // Compute number of replacements. if m := strings.Count(s, old); m == 0 { return nil, s // avoid allocation } else if n < 0 || m < n { n = m } // Apply replacements to buffer. t := make([]byte, len(s)+n*(len(new)-len(old))) w := 0 start := 0 var regionIDs []string for i := 0; i < n; i++ { j := start if len(old) == 0 { if i > 0 { _, wid := utf8.DecodeRuneInString(s[start:]) j += wid } } else { j += strings.Index(s[start:], old) } w += copy(t[w:], s[start:j]) w += copy(t[w:], fmt.Sprintf(new, i)) regionIDs = append(regionIDs, strconv.Itoa(i)) start = j + len(old) } w += copy(t[w:], s[start:]) return regionIDs, string(t[0:w]) } ================================================ FILE: utils/utils.go ================================================ package utils import ( "io" "io/ioutil" "log" "os" "os/exec" "strings" ) func Edit(contents *string) error { f, err := ioutil.TempFile("", "*.md") if err != nil { return err } defer os.Remove(f.Name()) if *contents != "" { if _, err := io.Copy(f, strings.NewReader(*contents)); err != nil { log.Println(err) } } f.Close() editor := os.Getenv("EDITOR") if editor == "" { editor = "vim" } cmd := exec.Command(editor, f.Name()) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return err } b, err := ioutil.ReadFile(f.Name()) if err != nil { return err } newContents := string(b) *contents = newContents return nil }