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  }