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  }