github.com/andrewhsu/cli/v2@v2.0.1-0.20210910131313-d4b4061f5b89/pkg/cmd/gist/view/view.go (about) 1 package view 2 3 import ( 4 "fmt" 5 "net/http" 6 "sort" 7 "strings" 8 "time" 9 10 "github.com/AlecAivazis/survey/v2" 11 "github.com/andrewhsu/cli/v2/internal/config" 12 "github.com/andrewhsu/cli/v2/internal/ghinstance" 13 "github.com/andrewhsu/cli/v2/pkg/cmd/gist/shared" 14 "github.com/andrewhsu/cli/v2/pkg/cmdutil" 15 "github.com/andrewhsu/cli/v2/pkg/iostreams" 16 "github.com/andrewhsu/cli/v2/pkg/markdown" 17 "github.com/andrewhsu/cli/v2/pkg/prompt" 18 "github.com/andrewhsu/cli/v2/pkg/text" 19 "github.com/andrewhsu/cli/v2/utils" 20 "github.com/spf13/cobra" 21 ) 22 23 type browser interface { 24 Browse(string) error 25 } 26 27 type ViewOptions struct { 28 IO *iostreams.IOStreams 29 Config func() (config.Config, error) 30 HttpClient func() (*http.Client, error) 31 Browser browser 32 33 Selector string 34 Filename string 35 Raw bool 36 Web bool 37 ListFiles bool 38 } 39 40 func NewCmdView(f *cmdutil.Factory, runF func(*ViewOptions) error) *cobra.Command { 41 opts := &ViewOptions{ 42 IO: f.IOStreams, 43 Config: f.Config, 44 HttpClient: f.HttpClient, 45 Browser: f.Browser, 46 } 47 48 cmd := &cobra.Command{ 49 Use: "view [<id> | <url>]", 50 Short: "View a gist", 51 Long: `View the given gist or select from recent gists.`, 52 Args: cobra.MaximumNArgs(1), 53 RunE: func(cmd *cobra.Command, args []string) error { 54 if len(args) == 1 { 55 opts.Selector = args[0] 56 } 57 58 if !opts.IO.IsStdoutTTY() { 59 opts.Raw = true 60 } 61 62 if runF != nil { 63 return runF(opts) 64 } 65 return viewRun(opts) 66 }, 67 } 68 69 cmd.Flags().BoolVarP(&opts.Raw, "raw", "r", false, "Print raw instead of rendered gist contents") 70 cmd.Flags().BoolVarP(&opts.Web, "web", "w", false, "Open gist in the browser") 71 cmd.Flags().BoolVarP(&opts.ListFiles, "files", "", false, "List file names from the gist") 72 cmd.Flags().StringVarP(&opts.Filename, "filename", "f", "", "Display a single file from the gist") 73 74 return cmd 75 } 76 77 func viewRun(opts *ViewOptions) error { 78 gistID := opts.Selector 79 client, err := opts.HttpClient() 80 if err != nil { 81 return err 82 } 83 84 cfg, err := opts.Config() 85 if err != nil { 86 return err 87 } 88 89 hostname, err := cfg.DefaultHost() 90 if err != nil { 91 return err 92 } 93 94 cs := opts.IO.ColorScheme() 95 if gistID == "" { 96 gistID, err = promptGists(client, hostname, cs) 97 if err != nil { 98 return err 99 } 100 101 if gistID == "" { 102 fmt.Fprintln(opts.IO.Out, "No gists found.") 103 return nil 104 } 105 } 106 107 if opts.Web { 108 gistURL := gistID 109 if !strings.Contains(gistURL, "/") { 110 gistURL = ghinstance.GistPrefix(hostname) + gistID 111 } 112 if opts.IO.IsStderrTTY() { 113 fmt.Fprintf(opts.IO.ErrOut, "Opening %s in your browser.\n", utils.DisplayURL(gistURL)) 114 } 115 return opts.Browser.Browse(gistURL) 116 } 117 118 if strings.Contains(gistID, "/") { 119 id, err := shared.GistIDFromURL(gistID) 120 if err != nil { 121 return err 122 } 123 gistID = id 124 } 125 126 gist, err := shared.GetGist(client, hostname, gistID) 127 if err != nil { 128 return err 129 } 130 131 theme := opts.IO.DetectTerminalTheme() 132 markdownStyle := markdown.GetStyle(theme) 133 if err := opts.IO.StartPager(); err != nil { 134 fmt.Fprintf(opts.IO.ErrOut, "starting pager failed: %v\n", err) 135 } 136 defer opts.IO.StopPager() 137 138 render := func(gf *shared.GistFile) error { 139 if shared.IsBinaryContents([]byte(gf.Content)) { 140 if len(gist.Files) == 1 || opts.Filename != "" { 141 return fmt.Errorf("error: file is binary") 142 } 143 _, err = fmt.Fprintln(opts.IO.Out, cs.Gray("(skipping rendering binary content)")) 144 return nil 145 } 146 147 if strings.Contains(gf.Type, "markdown") && !opts.Raw { 148 rendered, err := markdown.Render(gf.Content, markdownStyle) 149 if err != nil { 150 return err 151 } 152 _, err = fmt.Fprint(opts.IO.Out, rendered) 153 return err 154 } 155 156 if _, err := fmt.Fprint(opts.IO.Out, gf.Content); err != nil { 157 return err 158 } 159 if !strings.HasSuffix(gf.Content, "\n") { 160 _, err := fmt.Fprint(opts.IO.Out, "\n") 161 return err 162 } 163 164 return nil 165 } 166 167 if opts.Filename != "" { 168 gistFile, ok := gist.Files[opts.Filename] 169 if !ok { 170 return fmt.Errorf("gist has no such file: %q", opts.Filename) 171 } 172 return render(gistFile) 173 } 174 175 if gist.Description != "" && !opts.ListFiles { 176 fmt.Fprintf(opts.IO.Out, "%s\n\n", cs.Bold(gist.Description)) 177 } 178 179 showFilenames := len(gist.Files) > 1 180 filenames := make([]string, 0, len(gist.Files)) 181 for fn := range gist.Files { 182 filenames = append(filenames, fn) 183 } 184 185 sort.Slice(filenames, func(i, j int) bool { 186 return strings.ToLower(filenames[i]) < strings.ToLower(filenames[j]) 187 }) 188 189 if opts.ListFiles { 190 for _, fn := range filenames { 191 fmt.Fprintln(opts.IO.Out, fn) 192 } 193 return nil 194 } 195 196 for i, fn := range filenames { 197 if showFilenames { 198 fmt.Fprintf(opts.IO.Out, "%s\n\n", cs.Gray(fn)) 199 } 200 if err := render(gist.Files[fn]); err != nil { 201 return err 202 } 203 if i < len(filenames)-1 { 204 fmt.Fprint(opts.IO.Out, "\n") 205 } 206 } 207 208 return nil 209 } 210 211 func promptGists(client *http.Client, host string, cs *iostreams.ColorScheme) (gistID string, err error) { 212 gists, err := shared.ListGists(client, host, 10, "all") 213 if err != nil { 214 return "", err 215 } 216 217 if len(gists) == 0 { 218 return "", nil 219 } 220 221 var opts []string 222 var result int 223 var gistIDs = make([]string, len(gists)) 224 225 for i, gist := range gists { 226 gistIDs[i] = gist.ID 227 description := "" 228 gistName := "" 229 230 if gist.Description != "" { 231 description = gist.Description 232 } 233 234 filenames := make([]string, 0, len(gist.Files)) 235 for fn := range gist.Files { 236 filenames = append(filenames, fn) 237 } 238 sort.Strings(filenames) 239 gistName = filenames[0] 240 241 gistTime := utils.FuzzyAgo(time.Since(gist.UpdatedAt)) 242 // TODO: support dynamic maxWidth 243 description = text.Truncate(100, text.ReplaceExcessiveWhitespace(description)) 244 opt := fmt.Sprintf("%s %s %s", cs.Bold(gistName), description, cs.Gray(gistTime)) 245 opts = append(opts, opt) 246 } 247 248 questions := &survey.Select{ 249 Message: "Select a gist", 250 Options: opts, 251 } 252 253 err = prompt.SurveyAskOne(questions, &result) 254 255 if err != nil { 256 return "", err 257 } 258 259 return gistIDs[result], nil 260 }