1package cli
2
3import (
4	"bufio"
5	cryptorand "crypto/rand"
6	"errors"
7	"fmt"
8	"io"
9	"math"
10	"math/big"
11	"math/rand"
12	"os"
13	"strconv"
14	"strings"
15	"time"
16
17	cowsay "github.com/Code-Hex/Neo-cowsay/v2"
18	"github.com/Code-Hex/Neo-cowsay/v2/internal/decoration"
19	"github.com/Code-Hex/Neo-cowsay/v2/internal/super"
20	"github.com/Code-Hex/go-wordwrap"
21	"github.com/jessevdk/go-flags"
22	"github.com/ktr0731/go-fuzzyfinder"
23	"github.com/mattn/go-colorable"
24)
25
26func init() {
27	// safely set the seed globally so we generate random ids. Tries to use a
28	// crypto seed before falling back to time.
29	var seed int64
30	cryptoseed, err := cryptorand.Int(cryptorand.Reader, big.NewInt(math.MaxInt64))
31	if err != nil {
32		// This should not happen, but worst-case fallback to time-based seed.
33		seed = time.Now().UnixNano()
34	} else {
35		seed = cryptoseed.Int64()
36	}
37	rand.Seed(seed)
38}
39
40// options struct for parse command line arguments
41type options struct {
42	Help     bool   `short:"h"`
43	Eyes     string `short:"e"`
44	Tongue   string `short:"T"`
45	Width    int    `short:"W"`
46	Borg     bool   `short:"b"`
47	Dead     bool   `short:"d"`
48	Greedy   bool   `short:"g"`
49	Paranoia bool   `short:"p"`
50	Stoned   bool   `short:"s"`
51	Tired    bool   `short:"t"`
52	Wired    bool   `short:"w"`
53	Youthful bool   `short:"y"`
54	List     bool   `short:"l"`
55	NewLine  bool   `short:"n"`
56	File     string `short:"f"`
57	Bold     bool   `long:"bold"`
58	Super    bool   `long:"super"`
59	Random   bool   `long:"random"`
60	Rainbow  bool   `long:"rainbow"`
61	Aurora   bool   `long:"aurora"`
62}
63
64// CLI prepare for running command-line.
65type CLI struct {
66	Version  string
67	Thinking bool
68	stderr   io.Writer
69	stdout   io.Writer
70	stdin    io.Reader
71}
72
73func (c *CLI) program() string {
74	if c.Thinking {
75		return "cowthink"
76	}
77	return "cowsay"
78}
79
80// Run runs command-line.
81func (c *CLI) Run(argv []string) int {
82	if c.stderr == nil {
83		c.stderr = os.Stderr
84	}
85	if c.stdout == nil {
86		c.stdout = colorable.NewColorableStdout()
87	}
88	if c.stdin == nil {
89		c.stdin = os.Stdin
90	}
91	if err := c.mow(argv); err != nil {
92		fmt.Fprintf(c.stderr, "%s: %s\n", c.program(), err.Error())
93		return 1
94	}
95	return 0
96}
97
98// mow will parsing for cowsay command line arguments and invoke cowsay.
99func (c *CLI) mow(argv []string) error {
100	var opts options
101	args, err := c.parseOptions(&opts, argv)
102	if err != nil {
103		return err
104	}
105
106	if opts.List {
107		cowPaths, err := cowsay.Cows()
108		if err != nil {
109			return err
110		}
111		for _, cowPath := range cowPaths {
112			if cowPath.LocationType == cowsay.InBinary {
113				fmt.Fprintf(c.stdout, "Cow files in binary:\n")
114			} else {
115				fmt.Fprintf(c.stdout, "Cow files in %s:\n", cowPath.Name)
116			}
117			fmt.Fprintln(c.stdout, wordwrap.WrapString(strings.Join(cowPath.CowFiles, " "), 80))
118			fmt.Fprintln(c.stdout)
119		}
120		return nil
121	}
122
123	if err := c.mowmow(&opts, args); err != nil {
124		return err
125	}
126
127	return nil
128}
129
130func (c *CLI) parseOptions(opts *options, argv []string) ([]string, error) {
131	p := flags.NewParser(opts, flags.None)
132	args, err := p.ParseArgs(argv)
133	if err != nil {
134		return nil, err
135	}
136
137	if opts.Help {
138		c.stdout.Write(c.usage())
139		os.Exit(0)
140	}
141
142	return args, nil
143}
144
145func (c *CLI) usage() []byte {
146	year := strconv.Itoa(time.Now().Year())
147	return []byte(c.program() + ` version ` + c.Version + `, (c) ` + year + ` codehex
148Usage: ` + c.program() + ` [-bdgpstwy] [-h] [-e eyes] [-f cowfile] [--random]
149          [-l] [-n] [-T tongue] [-W wrapcolumn]
150          [--bold] [--rainbow] [--aurora] [--super] [message]
151
152Original Author: (c) 1999 Tony Monroe
153`)
154}
155
156func (c *CLI) generateOptions(opts *options) []cowsay.Option {
157	o := make([]cowsay.Option, 0, 8)
158	if opts.File == "-" {
159		cows := cowList()
160		idx, _ := fuzzyfinder.Find(cows, func(i int) string {
161			return cows[i]
162		})
163		opts.File = cows[idx]
164	}
165	o = append(o, cowsay.Type(opts.File))
166	if c.Thinking {
167		o = append(o,
168			cowsay.Thinking(),
169			cowsay.Thoughts('o'),
170		)
171	}
172	if opts.Random {
173		o = append(o, cowsay.Random())
174	}
175	if opts.Eyes != "" {
176		o = append(o, cowsay.Eyes(opts.Eyes))
177	}
178	if opts.Tongue != "" {
179		o = append(o, cowsay.Tongue(opts.Tongue))
180	}
181	if opts.Width > 0 {
182		o = append(o, cowsay.BallonWidth(uint(opts.Width)))
183	}
184	if opts.NewLine {
185		o = append(o, cowsay.DisableWordWrap())
186	}
187	return selectFace(opts, o)
188}
189
190func cowList() []string {
191	cows, err := cowsay.Cows()
192	if err != nil {
193		return cowsay.CowsInBinary()
194	}
195	list := make([]string, 0)
196	for _, cow := range cows {
197		list = append(list, cow.CowFiles...)
198	}
199	return list
200}
201
202func (c *CLI) phrase(opts *options, args []string) string {
203	if len(args) > 0 {
204		return strings.Join(args, " ")
205	}
206	lines := make([]string, 0, 40)
207	scanner := bufio.NewScanner(c.stdin)
208	for scanner.Scan() {
209		lines = append(lines, scanner.Text())
210	}
211	return strings.Join(lines, "\n")
212}
213
214func (c *CLI) mowmow(opts *options, args []string) error {
215	phrase := c.phrase(opts, args)
216	o := c.generateOptions(opts)
217	if opts.Super {
218		return super.RunSuperCow(phrase, opts.Bold, o...)
219	}
220
221	say, err := cowsay.Say(phrase, o...)
222	if err != nil {
223		var notfound *cowsay.NotFound
224		if errors.As(err, &notfound) {
225			return fmt.Errorf("could not find %s cowfile", notfound.Cowfile)
226		}
227		return err
228	}
229
230	options := make([]decoration.Option, 0)
231
232	if opts.Bold {
233		options = append(options, decoration.WithBold())
234	}
235	if opts.Rainbow {
236		options = append(options, decoration.WithRainbow())
237	}
238	if opts.Aurora {
239		options = append(options, decoration.WithAurora(rand.Intn(256)))
240	}
241
242	w := decoration.NewWriter(c.stdout, options...)
243	fmt.Fprintln(w, say)
244
245	return nil
246}
247
248func selectFace(opts *options, o []cowsay.Option) []cowsay.Option {
249	switch {
250	case opts.Borg:
251		o = append(o,
252			cowsay.Eyes("=="),
253			cowsay.Tongue("  "),
254		)
255	case opts.Dead:
256		o = append(o,
257			cowsay.Eyes("xx"),
258			cowsay.Tongue("U "),
259		)
260	case opts.Greedy:
261		o = append(o,
262			cowsay.Eyes("$$"),
263			cowsay.Tongue("  "),
264		)
265	case opts.Paranoia:
266		o = append(o,
267			cowsay.Eyes("@@"),
268			cowsay.Tongue("  "),
269		)
270	case opts.Stoned:
271		o = append(o,
272			cowsay.Eyes("**"),
273			cowsay.Tongue("U "),
274		)
275	case opts.Tired:
276		o = append(o,
277			cowsay.Eyes("--"),
278			cowsay.Tongue("  "),
279		)
280	case opts.Wired:
281		o = append(o,
282			cowsay.Eyes("OO"),
283			cowsay.Tongue("  "),
284		)
285	case opts.Youthful:
286		o = append(o,
287			cowsay.Eyes(".."),
288			cowsay.Tongue("  "),
289		)
290	}
291	return o
292}
293