├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── .goreleaser.yml
├── .pre-commit-config.yaml
├── LICENSE
├── README.md
├── cmd
├── client.go
├── root.go
├── ui.go
└── ui_test.go
├── gh-s-logo.png
├── go.mod
├── go.sum
└── main.go
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: release
2 | on:
3 | push:
4 | tags:
5 | - "v*"
6 |
7 | jobs:
8 | goreleaser:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: Checkout
12 | uses: actions/checkout@v3
13 | with:
14 | fetch-depth: 0
15 | - name: Set up Go
16 | uses: actions/setup-go@v3
17 | - name: Run GoReleaser
18 | uses: goreleaser/goreleaser-action@v4
19 | with:
20 | distribution: goreleaser
21 | version: latest
22 | args: release --clean
23 | env:
24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /gh-s
2 | /gh-s.exe
3 |
--------------------------------------------------------------------------------
/.goreleaser.yml:
--------------------------------------------------------------------------------
1 | before:
2 | hooks:
3 | - go mod tidy
4 | builds:
5 | - env:
6 | - CGO_ENABLED=0
7 | goos:
8 | - darwin
9 | - linux
10 | - windows
11 | - netbsd
12 | archives:
13 | - name_template: "{{ .Os }}-{{ .Arch }}"
14 | format: binary
15 | snapshot:
16 | name_template: "{{ .Tag }}-next"
17 | changelog:
18 | use: github-native
19 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/dnephin/pre-commit-golang
3 | rev: v0.4.0
4 | hooks:
5 | - id: go-fmt
6 | - id: go-unit-tests
7 | - id: go-build
8 | - id: go-mod-tidy
9 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright [yyyy] [name of copyright owner]
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
18 |
19 | search github repositories interactively
20 |
25 |
26 | Search GitHub repositories interactively from the command line. Start the prompt and browse the results! The name of that repository 🤔? Written in rust, a list of awesome projects...
27 |
28 | ...well say no more:
29 | ```
30 | gh s -l rust -d list
31 | ```
32 |
33 |
34 |
35 | ## Installation
36 | ```
37 | gh extension install gennaro-tedesco/gh-s
38 | ```
39 | This being a `gh` extension, you of course need [gh cli](https://github.com/cli/cli) as prerequisite.
40 |
41 | ## Usage
42 | Get started!
43 | ```
44 | gh s
45 | ```
46 |
47 | 
48 |
49 | ...or do you prefer a [full YouTube video](https://www.youtube.com/watch?v=JbG_mLsbw24) on the topic?
50 |
51 | Without any argument (or with flags only) `gh s` starts a prompt to insert the search query; after the search a list of results is shown. Navigate the list to show details, stars counts, URL and more. If instead you want to do all in one line
52 | ```
53 | gh s [search] [flag]
54 | ```
55 | takes one of the following arguments or flags
56 |
57 | | flags | description | multiple | example |
58 | | :------------ | :----------------------------------------------- | :-------- | :--------------------------- |
59 | | -E, --empty | do not prompt for name, search by flags only | no | gh s -E -l go -l rust |
60 | | -l, --lang | narrow down the search to a specific language | yes (OR) | gh s prompt -l go -l lua |
61 | | -d, --desc | search for keyword in the repository description | no | gh s neovim -d plugin |
62 | | -u, --user | restrict the search to a specific user | no | gh s lsp -u neovim |
63 | | -t, --topic | narrow down the search to specific topics | yes (AND) | gh s lsp -t plugin -t neovim |
64 | | -c, --colour | change colour of the prompt | no | gh s nvim -c magenta |
65 | | -L, --limit | limit the number of results (default 20) | no | gh s nvim -L 3 |
66 | | -h, --help | show the help page | no | gh s -h |
67 | | -V, --version | print the current version | no | gh s -V |
68 |
69 | The prompt accepts the following navigation commands:
70 |
71 | | key | description |
72 | | :------------- | :---------------------------------------- |
73 | | arrow keys | browse results list |
74 | | `/` | toggle search in results list |
75 | | `enter ()` | print selected repository URL to `stdout` |
76 |
77 | ### Search by topic or language only
78 | `gh-s` allows to skip prompting for a repository name by passing the `-E` flag; this in turn implies that the query searches against all possible GitHub repositories, which may result in longer response times. Notice furthermore that `-E` must always be accompanied by at least another non-empty flag. Use with care, however it does allow for some interesting statistics or general curiosity: check the [Wiki](https://github.com/gennaro-tedesco/gh-s/wiki/Common-queries)!
79 |
80 | ### Execute commands
81 | `gh-s` must be intended as a filter prompt returning the URL of the selection; as such, the best and most flexible way to execute commands with the results is to pipe it into and from `stdin/stdout`. Have a look at the [Wiki](https://github.com/gennaro-tedesco/gh-s/wiki/Execute-commands) for some common examples!
82 |
83 | ## Feedback
84 | If you find this application useful consider awarding it a ⭐, it is a great way to give feedback! Otherwise, any additional suggestions or merge request is warmly welcome!
85 |
86 | See also the complete family of extensions
87 | - [gh-i](https://github.com/gennaro-tedesco/gh-i) to search for github issues with interactive prompt
88 | - [gh-f](https://github.com/gennaro-tedesco/gh-f) to snap around your git worfklow with `fzf`
89 |
--------------------------------------------------------------------------------
/cmd/client.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "log"
5 | "net/url"
6 |
7 | gh "github.com/cli/go-gh"
8 | )
9 |
10 | type repoInfo struct {
11 | Name string
12 | Description string
13 | URL string
14 | Stars float64
15 | }
16 |
17 | func checkNil(decoded interface{}, key string) string {
18 | val, ok := decoded.(map[string]interface{})[key]
19 | if ok && val != nil {
20 | return val.(string)
21 | }
22 | return ""
23 | }
24 |
25 | func getRepos(query url.Values) []repoInfo {
26 | client, err := gh.RESTClient(nil)
27 | if err != nil {
28 | log.Fatal(err)
29 | }
30 |
31 | var apiResults map[string]interface{}
32 | err = client.Get("search/repositories?"+query.Encode(), &apiResults)
33 | if err != nil {
34 | log.Println("\033[31m ✘\033[0m Perhaps you mispelt some flags?")
35 | log.Fatal(err)
36 | }
37 |
38 | itemsResults := apiResults["items"].([]interface{})
39 |
40 | var repos []repoInfo
41 | for _, item := range itemsResults {
42 | repos = append(repos, repoInfo{
43 | Name: item.(map[string]interface{})["full_name"].(string),
44 | Description: checkNil(item, "description"),
45 | URL: item.(map[string]interface{})["html_url"].(string),
46 | Stars: item.(map[string]interface{})["stargazers_count"].(float64),
47 | })
48 | }
49 | return repos
50 | }
51 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | "github.com/spf13/cobra"
8 | )
9 |
10 | var cfgFile string
11 |
12 | // VERSION number: change manually
13 | const VERSION = "0.0.8"
14 |
15 | var rootCmd = &cobra.Command{
16 | Use: "gh-s",
17 | Short: "gh-s: search repositories interactively",
18 | Long: "gh-s: interactive prompt to search and browse github repositories",
19 | Args: cobra.MaximumNArgs(1),
20 | Run: func(cmd *cobra.Command, args []string) {
21 | version, _ := cmd.Flags().GetBool("version")
22 | if version {
23 | fmt.Println("gh-s", VERSION)
24 | os.Exit(1)
25 | }
26 | languageList, _ := cmd.Flags().GetStringSlice("lang")
27 | desc, _ := cmd.Flags().GetString("desc")
28 | user, _ := cmd.Flags().GetString("user")
29 | topicList, _ := cmd.Flags().GetStringSlice("topic")
30 | colour, _ := cmd.Flags().GetString("colour")
31 | limit, _ := cmd.Flags().GetInt("limit")
32 |
33 | searchString := func() string {
34 | if empty, _ := (cmd.Flags().GetBool("empty")); empty {
35 | if isEmptyQuery(user, languageList, topicList) {
36 | fmt.Println("\033[31m ✘\033[0m -E flag is only allowed together with -u, -l or -t")
37 | os.Exit(1)
38 | }
39 | return ""
40 | }
41 | return getSearchString(args)
42 | }()
43 | parsedQuery := parseInput(searchString, languageList, desc, user, topicList)
44 | repos := getRepos(parsedQuery)
45 | if len(repos) == 0 {
46 | fmt.Println("\033[31m ✘\033[0m No results found")
47 | os.Exit(1)
48 | }
49 | PromptList := getSelectionPrompt(repos, colour, limit)
50 |
51 | idx, _, err := PromptList.Run()
52 | if err != nil {
53 | fmt.Println(err)
54 | os.Exit(1)
55 | }
56 | fmt.Println(repos[idx].URL)
57 | },
58 | }
59 |
60 | func Execute() {
61 | cobra.CheckErr(rootCmd.Execute())
62 | }
63 |
64 | func init() {
65 | var topics []string
66 | var languages []string
67 | rootCmd.Flags().StringSliceVarP(&languages, "lang", "l", []string{}, "specify repository language")
68 | rootCmd.Flags().StringP("desc", "d", "", "search in repository description")
69 | rootCmd.Flags().StringP("user", "u", "", "search repository by user")
70 | rootCmd.Flags().StringSliceVarP(&topics, "topic", "t", []string{}, "search repository by topic")
71 | rootCmd.Flags().StringP("colour", "c", "cyan", "colour of selection prompt")
72 | rootCmd.Flags().IntP("limit", "L", 20, "limit the number of results (default 20)")
73 | rootCmd.Flags().BoolP("empty", "E", false, "allow for empty name search")
74 | rootCmd.Flags().BoolP("version", "V", false, "print current version")
75 | rootCmd.SetHelpTemplate(getRootHelp())
76 | }
77 |
78 | func isEmptyQuery(user string, languageList []string, topicList []string) bool {
79 | return (user == "") && (len(languageList) == 0) && (len(topicList) == 0)
80 | }
81 |
82 | func getRootHelp() string {
83 | return `
84 | gh-s: search repositories interactively. The search returns results
85 | matching the indicated query ordered by number of repository stars.
86 |
87 | Synopsis:
88 | gh s [search] [flags]
89 |
90 | Usage:
91 | gh s
92 |
93 | if no arguments or flags are given, show an interactive prompt
94 | to search, browse and filter repositories. Selecting an entry
95 | from the list returns its address to stdout, so that it can be
96 | piped into execution commands: generally you want to do
97 |
98 | gh s [search] [flags] | xargs -n1 ...
99 |
100 | check the wiki for examples: https://github.com/gennaro-tedesco/gh-s/wiki/Execute-commands
101 |
102 | Flags can be passed so that the search is narrowed down (see available
103 | flags below). For example:
104 |
105 | gh s -l lua -d quickfix
106 |
107 | If you provide an argument before the flags the prompt is skipped and such
108 | argument is used in the name field to search for repositories:
109 |
110 | gh s ripgrep -l rust
111 |
112 | Prompt commands:
113 |
114 | arrow keys : move up and down the list
115 | / : toggle fuzzy search
116 | enter (): return selected repository to stdout
117 |
118 | Flags:
119 | -E, --empty allow to pass an empty string as name, that is search
120 | github repositories based on topic and language only.
121 | For this to work at least one other flag must be non-empty.
122 | -l, --lang search repositories with specific language
123 | multiple languages can be specified:
124 | -l go -l rust -l lua
125 | -d, --desc match repository description
126 | -u, --user narrow the search down to a specific user's repositories
127 | -t, --topic search for topics in repositories
128 | multiple topics can be specified:
129 | -t go -t gh-extension
130 | -c, --colour change prompt colour
131 | -L, --limit limit the number of results (default 20)
132 | -V, --version print current version
133 | -h, --help show this help page
134 |
135 | Examples:
136 |
137 | # search for name=ripgrep and language=rust
138 | gh s ripgrep -l rust
139 |
140 | # what is the most starred neovim plugin?
141 | gh s neovim -d plugin
142 |
143 | # restrict to one user only
144 | gh s lsp -u neovim
145 |
146 | # all neovim plugins in lua of nvim-*
147 | gh s nvim -t plugin -l lua
148 |
149 | # the most famous go or rust frameworks
150 | gh s -E -l go -l rust
151 |
152 | # list all your repositories
153 | gh s -E -u @me
154 | `
155 | }
156 |
--------------------------------------------------------------------------------
/cmd/ui.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "log"
7 | "net/url"
8 | "os"
9 | "strings"
10 |
11 | "github.com/manifoldco/promptui"
12 | )
13 |
14 | func getSearchString(args []string) string {
15 | if len(args) == 0 {
16 | prompt := promptui.Prompt{
17 | Stdout: os.Stderr,
18 | Stdin: os.Stdin,
19 | Label: "Repository name",
20 | Validate: func(input string) error {
21 | if len(input) == 0 {
22 | return errors.New("no input provided")
23 | }
24 | return nil
25 | },
26 | }
27 |
28 | result, err := prompt.Run()
29 | if err != nil {
30 | log.Fatal(err)
31 | }
32 | return result
33 | }
34 | return args[0]
35 | }
36 |
37 | func parseInput(search string, languageList []string, desc string, user string, topicList []string) url.Values {
38 | queryString := fmt.Sprintf("%s in:name", search)
39 | for _, language := range languageList {
40 | queryString = queryString + fmt.Sprintf(" language:%s", language)
41 | }
42 | if desc != "" {
43 | queryString = queryString + fmt.Sprintf(" %s in:description", desc)
44 | }
45 | if user != "" {
46 | queryString = queryString + fmt.Sprintf(" user:%s", user)
47 | }
48 | for _, topic := range topicList {
49 | queryString = queryString + fmt.Sprintf(" topic:%s", topic)
50 | }
51 | query := url.Values{}
52 | query.Add("q", queryString)
53 | query.Add("sort", "stars")
54 | query.Add("per_page", "100")
55 | return query
56 | }
57 |
58 | func getTemplate(colour string) *promptui.SelectTemplates {
59 | funcMap := promptui.FuncMap
60 | funcMap["parseStars"] = func(starCount float64) string {
61 | if starCount >= 1000 {
62 | return fmt.Sprintf("%.1f k", starCount/1000)
63 | }
64 | return fmt.Sprint(starCount)
65 | }
66 |
67 | funcMap["truncate"] = func(input string) string {
68 | length := 80
69 | if len(input) <= length {
70 | return input
71 | }
72 | return input[:length-3] + "..."
73 | }
74 |
75 | return &promptui.SelectTemplates{
76 | Active: fmt.Sprintf("\U0001F449 {{ .Name | %s | bold }}", colour),
77 | Inactive: fmt.Sprintf(" {{ .Name | %s }}", colour),
78 | Selected: fmt.Sprintf(`{{ "✔" | green | bold }} {{ .Name | %s | bold }}`, colour),
79 | Details: `
80 | {{ "Name:" | faint }} {{ .Name }}
81 | {{ "Description:" | faint }} {{ .Description | truncate }}
82 | {{ "Url address:" | faint }} {{ .URL }}
83 | {{ "⭐" | faint }} {{ .Stars | parseStars }}`,
84 | }
85 |
86 | }
87 |
88 | func getSelectionPrompt(repos []repoInfo, colour string, limit int) *promptui.Select {
89 | return &promptui.Select{
90 | Stdout: os.Stderr,
91 | Stdin: os.Stdin,
92 | Label: "repository list",
93 | Items: repos,
94 | Templates: getTemplate(colour),
95 | Size: limit,
96 | Searcher: func(input string, idx int) bool {
97 | repo := repos[idx]
98 | title := strings.ToLower(repo.Name)
99 |
100 | return strings.Contains(title, input)
101 | },
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/cmd/ui_test.go:
--------------------------------------------------------------------------------
1 | package cmd
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/stretchr/testify/assert"
7 | )
8 |
9 | func TestParseInput(t *testing.T) {
10 | inputValues := map[string]interface{}{
11 | "search": "awesome",
12 | "languageList": []string{"go", "lua"},
13 | "desc": "framework",
14 | "user": "@me",
15 | "topicList": []string{"cli", "gh-extension"},
16 | }
17 |
18 | trueString := "awesome in:name language:go language:lua framework in:description user:@me topic:cli topic:gh-extension"
19 | parsedString := parseInput(inputValues["search"].(string), inputValues["languageList"].([]string), inputValues["desc"].(string), inputValues["user"].(string), inputValues["topicList"].([]string))
20 |
21 | assert.Equal(t, trueString, parsedString["q"][0])
22 | }
23 |
--------------------------------------------------------------------------------
/gh-s-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gennaro-tedesco/gh-s/be966ad3b6e449d4cc70e90fe5a3475433260176/gh-s-logo.png
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/gennaro-tedesco/gh-s
2 |
3 | go 1.18
4 |
5 | require (
6 | github.com/cli/go-gh v1.2.1
7 | github.com/manifoldco/promptui v0.9.0
8 | github.com/spf13/cobra v1.7.0
9 | github.com/stretchr/testify v1.7.0
10 | )
11 |
12 | require (
13 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
14 | github.com/chzyer/readline v1.5.1 // indirect
15 | github.com/cli/safeexec v1.0.1 // indirect
16 | github.com/cli/shurcooL-graphql v0.0.3 // indirect
17 | github.com/davecgh/go-spew v1.1.1 // indirect
18 | github.com/henvic/httpretty v0.1.0 // indirect
19 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
20 | github.com/kr/text v0.2.0 // indirect
21 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
22 | github.com/mattn/go-isatty v0.0.18 // indirect
23 | github.com/mattn/go-runewidth v0.0.14 // indirect
24 | github.com/muesli/termenv v0.15.1 // indirect
25 | github.com/pmezard/go-difflib v1.0.0 // indirect
26 | github.com/rivo/uniseg v0.4.4 // indirect
27 | github.com/spf13/pflag v1.0.5 // indirect
28 | github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e // indirect
29 | golang.org/x/net v0.8.0 // indirect
30 | golang.org/x/sys v0.7.0 // indirect
31 | golang.org/x/term v0.7.0 // indirect
32 | golang.org/x/tools v0.6.0 // indirect
33 | gopkg.in/yaml.v3 v3.0.1 // indirect
34 | )
35 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
2 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
3 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
4 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
5 | github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
6 | github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
7 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
8 | github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
9 | github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
10 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
11 | github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
12 | github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
13 | github.com/cli/go-gh v1.2.1 h1:xFrjejSsgPiwXFP6VYynKWwxLQcNJy3Twbu82ZDlR/o=
14 | github.com/cli/go-gh v1.2.1/go.mod h1:Jxk8X+TCO4Ui/GarwY9tByWm/8zp4jJktzVZNlTW5VM=
15 | github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00=
16 | github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
17 | github.com/cli/shurcooL-graphql v0.0.3 h1:CtpPxyGDs136/+ZeyAfUKYmcQBjDlq5aqnrDCW5Ghh8=
18 | github.com/cli/shurcooL-graphql v0.0.3/go.mod h1:tlrLmw/n5Q/+4qSvosT+9/W5zc8ZMjnJeYBxSdb4nWA=
19 | github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
20 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
21 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
22 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
23 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
25 | github.com/henvic/httpretty v0.1.0 h1:Htk66UUEbXTD4JR0qJZaw8YAMKw+9I24ZZOnDe/ti+E=
26 | github.com/henvic/httpretty v0.1.0/go.mod h1:ViEsly7wgdugYtymX54pYp6Vv2wqZmNHayJ6q8tlKCc=
27 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
28 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
29 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
30 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
31 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
32 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
33 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
34 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
35 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
36 | github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
37 | github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
38 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
39 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
40 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
41 | github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs=
42 | github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ=
43 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
44 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
45 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
46 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
47 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
48 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
49 | github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
50 | github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
51 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
52 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
53 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
54 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
55 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
56 | github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e h1:BuzhfgfWQbX0dWzYzT1zsORLnHRv3bcRcsaUk0VmXA8=
57 | github.com/thlib/go-timezone-local v0.0.0-20210907160436-ef149e42d28e/go.mod h1:/Tnicc6m/lsJE0irFMA0LfIwTBo4QP7A8IfyIv4zZKI=
58 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
59 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
60 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
61 | golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
62 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
63 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
64 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
65 | golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
66 | golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
67 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
68 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
69 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
70 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
71 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
72 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
73 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
74 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
75 | golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
76 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
77 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
78 | golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
79 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
80 | golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
81 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
82 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
83 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
84 | golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ=
85 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
86 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
87 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
88 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
89 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
90 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
91 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
92 | golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
93 | golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
94 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
95 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
96 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
97 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
98 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
99 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
100 | gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
101 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
102 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
103 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
104 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "github.com/gennaro-tedesco/gh-s/cmd"
4 |
5 | func main() {
6 | cmd.Execute()
7 | }
8 |
--------------------------------------------------------------------------------