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