diff --git a/go.mod b/go.mod index fe0d39b7b36..481441f05d7 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/briandowns/spinner v1.11.1 github.com/charmbracelet/glamour v0.2.1-0.20200724174618-1246d13c1684 github.com/enescakir/emoji v1.0.0 + github.com/gdamore/tcell/v2 v2.0.0 github.com/google/go-cmp v0.5.2 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/hashicorp/go-version v1.2.1 diff --git a/go.sum b/go.sum index b3659cb766b..5ff63fb8e36 100644 --- a/go.sum +++ b/go.sum @@ -68,6 +68,10 @@ github.com/enescakir/emoji v1.0.0/go.mod h1:Bt1EKuLnKDTYpLALApstIkAjdDrS/8IAgTkK github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.0.0 h1:GRWG8aLfWAlekj9Q6W29bVvkHENc6hp79XOqG4AWDOs= +github.com/gdamore/tcell/v2 v2.0.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= @@ -341,6 +345,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190530182044-ad28b68e88f1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/pkg/cmd/protip/protip.go b/pkg/cmd/protip/protip.go new file mode 100644 index 00000000000..683d09ebab8 --- /dev/null +++ b/pkg/cmd/protip/protip.go @@ -0,0 +1,188 @@ +package protip + +import ( + "fmt" + "strings" + "time" + + "github.com/cli/cli/pkg/cmdutil" + "github.com/cli/cli/pkg/iostreams" + "github.com/gdamore/tcell/v2" + "github.com/mattn/go-runewidth" + "github.com/spf13/cobra" +) + +type ProtipOptions struct { + IO *iostreams.IOStreams + + Character string + Sprites map[string]*Sprite + Sprite *Sprite +} + +func NewCmdProtip(f *cmdutil.Factory, runF func(*ProtipOptions) error) *cobra.Command { + opts := &ProtipOptions{ + IO: f.IOStreams, + Sprites: map[string]*Sprite{ + "clippy": Clippy(), + "manatee": Manatee(), + }, + } + + cmd := &cobra.Command{ + Use: "protip", + Short: "get a random protip about using gh", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + opts.Character = strings.ToLower(opts.Character) + if s, ok := opts.Sprites[opts.Character]; ok { + opts.Sprite = s + } else { + return fmt.Errorf("no such character: %s", opts.Character) + } + + if runF != nil { + return runF(opts) + } + return protipRun(opts) + }, + } + + cmd.Flags().StringVarP(&opts.Character, "character", "c", "clippy", "What helpful animated character you'd like a protip from") + + return cmd +} + +func protipRun(opts *ProtipOptions) error { + tcell.SetEncodingFallback(tcell.EncodingFallbackASCII) + + tip := getProtip() + + style := tcell.StyleDefault + + s, err := tcell.NewScreen() + if err != nil { + return err + } + if err = s.Init(); err != nil { + return err + } + + s.SetStyle(style. + Foreground(tcell.ColorGreen). + Background(tcell.ColorBlack)) + s.Clear() + + quit := make(chan struct{}) + go func() { + for { + ev := s.PollEvent() + switch ev := ev.(type) { + case *tcell.EventKey: + switch ev.Rune() { + case 'q': + close(quit) + return + } + switch ev.Key() { + case tcell.KeyEscape, tcell.KeyEnter: + close(quit) + return + case tcell.KeyCtrlL: + s.Sync() + } + case *tcell.EventResize: + s.Sync() + } + } + }() + +loop: + for { + select { + case <-quit: + break loop + case <-time.After(time.Millisecond * 500): + } + s.Clear() + drawSprite(s, 0, 0, style, opts.Sprite) + drawProtip(s, opts.Sprite.Width, 1, style, tip) + s.Show() + } + + s.Fini() + + return nil +} + +func drawProtip(s tcell.Screen, startX, startY int, st tcell.Style, tipLines []string) { + // Should look like this: + /* + + *--------------------------------* + | To merge a PR, review it until | + | it is approved. | + |________________________________* + / + + */ + + width := len(tipLines[0]) + 5 + pad := func(s string) string { + out := " | " + s + spaces := width - len(out) - 1 + for i := 0; i < spaces; i++ { + out += " " + } + out += "|" + + return out + } + + // draw top border + topBorder := " *" + bottomBorder := " |" + for x := 0; x < width-4; x++ { + topBorder += "-" + bottomBorder += "_" + } + topBorder += "-*" + bottomBorder += "_*" + + drawStr(s, startX, startY, st, topBorder) + + y := startY + 1 + + for iy, line := range tipLines { + drawStr(s, startX, y+iy, st, pad(line)) + y += iy + } + + drawStr(s, startX, y+1, st, bottomBorder) + drawStr(s, startX, y+2, st, "/") +} + +func drawStr(s tcell.Screen, x, y int, style tcell.Style, str string) { + for _, c := range str { + var comb []rune + w := runewidth.RuneWidth(c) + if w == 0 { + comb = []rune{c} + c = ' ' + w = 1 + } + s.SetContent(x, y, c, comb, style) + x += w + } +} + +func drawSprite(s tcell.Screen, x, y int, st tcell.Style, sp *Sprite) { + cells := sp.Cells() + for y := 0; y < len(cells); y++ { + row := cells[y] + for x := 0; x < len(row); x++ { + s.SetContent(x, y, cells[y][x], nil, st) + } + } + sp.IncrFrame() +} diff --git a/pkg/cmd/protip/protips.go b/pkg/cmd/protip/protips.go new file mode 100644 index 00000000000..042a246e439 --- /dev/null +++ b/pkg/cmd/protip/protips.go @@ -0,0 +1,30 @@ +package protip + +import ( + "math/rand" + "time" +) + +func getProtip() []string { + rand.Seed(time.Now().UnixNano()) + tips := [][]string{ + { + "To merge a pull request, review it", + "until it is approved", + }, + { + "To have gh use a different editor you can run:", + "gh config set editor ", + }, + { + "If you prefer to use gh over ssh you can run:", + "gh config set git_protocol ssh", + }, + { + "You can search through issues using grep:", + "gh issue list -L200 | grep 'search term'", + }, + } + + return tips[rand.Intn(len(tips))] +} diff --git a/pkg/cmd/protip/sprites.go b/pkg/cmd/protip/sprites.go new file mode 100644 index 00000000000..4a17097819a --- /dev/null +++ b/pkg/cmd/protip/sprites.go @@ -0,0 +1,209 @@ +package protip + +import "strings" + +type Sprite struct { + Width int + Height int + Frames []string + frameIx int +} + +func (s *Sprite) AddFrame(f string) { + s.Frames = append(s.Frames, f) +} + +func (s *Sprite) IncrFrame() { + s.frameIx = (s.frameIx + 1) % len(s.Frames) +} + +func (s *Sprite) Cells() [][]rune { + out := [][]rune{} + curFrame := s.Frames[s.frameIx] + lines := strings.Split(curFrame, "\n") + for _, line := range lines { + out = append(out, []rune(line)) + } + return out +} + +func Manatee() *Sprite { + s := &Sprite{ + Width: 40, + Height: 14, + } + + s.AddFrame(` + _.---.._ + _ _.-' ''-. + .' '-,_.-' '''. + ( _ o : + '._ .-' '-._ \ \- ---] + '-.___.-') )..-' + (_/ + + + +(art by MJP) +`) + s.AddFrame(` + + _.---.._ + _ _.-' ''-. + .' '-,_.-' '''. + ( _ o : + '._ .-' '-._ \ \- ---] + '-.___.-') )..-' + (_/ + + +(art by MJP) +`) + s.AddFrame(` + + + _.---.._ + _ _.-' ''-. + .' '-,_.-' '''. + ( _ o : + '._ .-' '-._ \ \- ---] + '-.___.-') )..-' + (_/ + +(art by MJP) +`) + s.AddFrame(` + + + + _.---.._ + _ _.-' ''-. + .' '-,_.-' '''. + ( _ o : + '._ .-' '-._ \ \- ---] + '-.___.-') )..-' + (_/ +(art by MJP) +`) + s.AddFrame(` + + + _.---.._ + _ _.-' ''-. + .' '-,_.-' '''. + ( _ o : + '._ .-' '-._ \ \- ---] + '-.___.-') )..-' + (_/ + +(art by MJP) +`) + s.AddFrame(` + + _.---.._ + _ _.-' ''-. + .' '-,_.-' '''. + ( _ o : + '._ .-' '-._ \ \- ---] + '-.___.-') )..-' + (_/ + + +(art by MJP) +`) + + return s +} + +func Clippy() *Sprite { + s := &Sprite{ + Width: 15, + Height: 15, + } + s.AddFrame(` + ___ + ^ ^ + O O + | | + || | + ||_/ / + | | + | | + \___/ + +`) + s.AddFrame(` + ___ + ^ ^ + O O + | | + || | + ||_/ - + | | + | | + \___/ + +`) + s.AddFrame(` + ___ + ^ ^ + O O + | | + || | + ||_/ / + | | + | | + \___/ + +`) + s.AddFrame(` + ___ + ^ ^ + o O + | | + || | + ||_/ / + | | + | | + \___/ + +`) + s.AddFrame(` + ___ + ^ ^ + - O + | | + || | + ||_/ / + | | + | | + \___/ + +`) + s.AddFrame(` + ___ + ^ ^ + o O + | | + || | + ||_/ / + | | + | | + \___/ + +`) + s.AddFrame(` + ___ + ^ ^ + O O + | | + || | + ||_/ / + | | + | | + \___/ + +`) + + return s +} diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index d1b5d9a9dad..bd8b6fb6d02 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -16,6 +16,7 @@ import ( gistCmd "github.com/cli/cli/pkg/cmd/gist" issueCmd "github.com/cli/cli/pkg/cmd/issue" prCmd "github.com/cli/cli/pkg/cmd/pr" + protipCmd "github.com/cli/cli/pkg/cmd/protip" releaseCmd "github.com/cli/cli/pkg/cmd/release" repoCmd "github.com/cli/cli/pkg/cmd/repo" creditsCmd "github.com/cli/cli/pkg/cmd/repo/credits" @@ -68,6 +69,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command { cmd.AddCommand(creditsCmd.NewCmdCredits(f, nil)) cmd.AddCommand(gistCmd.NewCmdGist(f)) cmd.AddCommand(completionCmd.NewCmdCompletion(f.IOStreams)) + cmd.AddCommand(protipCmd.NewCmdProtip(f, nil)) // the `api` command should not inherit any extra HTTP headers bareHTTPCmdFactory := *f