cowsay/main.go

256 lines
6.3 KiB
Go
Raw Normal View History

2024-03-28 01:41:34 +00:00
package main
import (
"bytes"
"embed"
"flag"
"fmt"
"io"
"log"
"os"
"strconv"
"strings"
"golang.org/x/term"
)
var (
// upper-left, upper-right, left side, right side, bottom-left, bottom-right
SAY_BORDER = []rune{'/', '\\', '|', '|', '\\', '/'}
SINGLE_BORDER = []rune{'<', '>'}
THINK_BORDER = []rune{'(', ')', '(', ')', '(', ')'}
)
type options struct {
thinking bool
tongue string
eyes string
leftEye string
rightEye string
wrapWidth int
}
// the cowfiles
//
//go:embed cows/*
var cowTemplates embed.FS
func main() {
// basic flags
cowFile := flag.String("cow", "default.cow", "the cowfile to use")
tongue := flag.String("T", " ", "the tongue to print")
eyesFlag := flag.String("e", "oo", "the eyes")
width := flag.Int("W", 40, "approximate column width for the word-wrap")
// also need to check for binary name here
thinkFlag := flag.Bool("think", false, "if the cow is thinking the input instead")
// flags for special conditions
// these flags override -e and -T if set
borgFlag := flag.Bool("b", false, "borg cow") // ==
deadFlag := flag.Bool("d", false, "dead cow") // xx AND tongue "U "
greedyFlag := flag.Bool("g", false, "greedy cow") // $$
paranoidFlag := flag.Bool("p", false, "paranoid cow") // @@
stonedFlag := flag.Bool("s", false, "stoned cow") // ** AND tongue "U "
tiredFlag := flag.Bool("t", false, "tired cow") // --
wiredFlag := flag.Bool("w", false, "wired cow") // OO
youngFlag := flag.Bool("y", false, "young cow") // ..
flag.Parse()
input := flag.Arg(0)
if len(*tongue) < 2 {
*tongue = *tongue + strings.Repeat(" ", 2-len(*tongue))
}
if len(*eyesFlag) < 2 {
*eyesFlag = *eyesFlag + strings.Repeat(" ", 2-len(*eyesFlag))
}
opts := options{
thinking: *thinkFlag,
tongue: (*tongue)[0:2],
eyes: (*eyesFlag)[0:2],
wrapWidth: *width,
}
switch {
case *borgFlag:
opts.eyes = "=="
case *deadFlag:
opts.eyes = "xx"
opts.tongue = "U "
case *greedyFlag:
opts.eyes = "$$"
case *paranoidFlag:
opts.eyes = "@@"
case *stonedFlag:
opts.eyes = "**"
opts.tongue = "U "
case *tiredFlag:
opts.eyes = "--"
case *wiredFlag:
opts.eyes = "OO"
case *youngFlag:
opts.eyes = ".."
}
opts.leftEye = strings.Split(*eyesFlag, "")[0]
opts.rightEye = strings.Split(*eyesFlag, "")[1]
// if there is command-line input, ignore stdin.
if !term.IsTerminal(0) && input != "" {
pipedInput, err := io.ReadAll(os.Stdin)
if err != nil {
log.Fatalf("error reading from stdin: %v", err)
}
input = strings.TrimSpace(string(pipedInput))
}
if input == "" {
log.Fatal("no input; cow can't say anything")
}
b, err := cowTemplates.ReadFile("cows/" + *cowFile)
if err != nil {
log.Fatal(err)
}
generateCow(string(b), input, opts)
}
func generateCow(cow, say string, opt options) string {
cowSlice := strings.Split(cow, "\n")
offset := 0
// Remove comments
i := 0
for _, s := range cowSlice {
if strings.HasPrefix(s, "##") && strings.Contains(s, "OFFSET") {
_, off, found := strings.Cut(s, "OFFSET ")
if found {
offset, _ = strconv.Atoi(off)
}
}
if !strings.HasPrefix(s, "##") {
cowSlice[i] = s
i++
}
}
cow = strings.Join(cowSlice[:i], "\n")
// do replaces
tail := `\`
if opt.thinking {
tail = `o`
}
cow = strings.ReplaceAll(cow, "$thoughts", tail)
cow = strings.ReplaceAll(cow, "$eyes", opt.eyes)
cow = strings.ReplaceAll(cow, "$el", opt.leftEye)
cow = strings.ReplaceAll(cow, "$er", opt.rightEye)
cow = strings.ReplaceAll(cow, "$tongue", opt.tongue)
out := &bytes.Buffer{}
fmt.Fprint(out, bubble(say, opt.thinking, opt.wrapWidth, offset))
fmt.Fprintln(out)
fmt.Fprint(out, cow)
fmt.Fprintln(out)
return out.String()
}
func wordWrap(text []string, column int) ([]string, int) {
out := []string{}
length := 0
var b strings.Builder
for _, line := range text {
b = strings.Builder{}
// remove control character whitespace
line = strings.ReplaceAll(line, "\t", " ")
line = strings.ReplaceAll(line, "\v", " ")
line = strings.ReplaceAll(line, "\f", " ")
words := strings.Split(line, " ")
// we -1 all over the place to clean up the loose space on the end of the builder
for _, word := range words { // split into words
if b.Len() > 0 && word == "" {
// skip multiple spaces in a row
continue
}
if b.Len() > 0 && b.Len()+len(word)+1 > column {
// if the word we want to append is bigger than we have room for,
// save off the maximum length, save the current line to the output,
// and start a new builder.
length = max(length, b.Len()-1)
out = append(out, b.String()[0:b.Len()-1])
b = strings.Builder{}
}
if b.Len() == 0 && len(word) >= column {
// our word is longer than our maximum column size. let's break it up.
length = max(length, column-1)
out = append(out, word[0:column-1])
word = word[column-1:]
b = strings.Builder{}
}
// actually append the word and a space
b.WriteString(word)
b.WriteRune(' ')
}
// out of words in the line, so save it off and start on the next one
length = max(length, b.Len()-1)
out = append(out, b.String()[0:b.Len()-1])
}
return out, length
}
func padText(text []string, maxLength int) []string {
for i, line := range text {
if len(line) < maxLength {
text[i] = text[i] + strings.Repeat(" ", maxLength-len(line))
}
}
return text
}
func bubble(text string, think bool, column int, offset int) string {
s, column := wordWrap(strings.Split(text, "\n"), column)
s = padText(s, column)
b := strings.Builder{}
padding := strings.Repeat(" ", offset)
b.WriteString(padding)
b.WriteString(" ")
b.WriteString(strings.Repeat("_", column+2))
b.WriteString(" \n")
var borderStyle []rune
if think {
borderStyle = THINK_BORDER
} else if len(s) == 1 {
borderStyle = SINGLE_BORDER
} else {
borderStyle = SAY_BORDER
}
for i := range s {
b.WriteString(padding)
if i == 0 {
b.WriteRune(borderStyle[0])
} else if i == len(s)-1 {
b.WriteRune(borderStyle[4])
} else {
b.WriteRune(borderStyle[2])
}
b.WriteRune(' ')
b.WriteString(s[i])
b.WriteRune(' ')
if i == 0 {
b.WriteRune(borderStyle[1])
} else if i == len(s)-1 {
b.WriteRune(borderStyle[5])
} else {
b.WriteRune(borderStyle[3])
}
b.WriteRune('\n')
}
b.WriteString(padding)
b.WriteString(" ")
b.WriteString(strings.Repeat("-", column+2))
b.WriteString(" ")
return b.String()
}