2024-03-28 01:41:34 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"embed"
|
|
|
|
"flag"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
"golang.org/x/term"
|
|
|
|
)
|
|
|
|
|
2024-03-31 00:10:41 +00:00
|
|
|
const DEBUG = false
|
|
|
|
|
2024-03-28 01:41:34 +00:00
|
|
|
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)
|
|
|
|
}
|
2024-03-31 00:10:41 +00:00
|
|
|
input = string(bytes.TrimSpace(pipedInput))
|
2024-03-28 01:41:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if input == "" {
|
|
|
|
log.Fatal("no input; cow can't say anything")
|
|
|
|
}
|
|
|
|
|
|
|
|
b, err := cowTemplates.ReadFile("cows/" + *cowFile)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatal(err)
|
|
|
|
}
|
|
|
|
|
2024-03-31 00:10:41 +00:00
|
|
|
fmt.Print(generateCow(string(b), input, opts))
|
2024-03-28 01:41:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2024-03-31 00:10:41 +00:00
|
|
|
// this is an attempt to emulate the rather poorly-documented behavior of Text::Wrap::fill
|
|
|
|
// from CPAN: https://github.com/ap/Text-Tabs/blob/master/lib.modern/Text/Wrap.pm
|
|
|
|
// The basic idea is: take an input. Format each "paragraph" of text independently,
|
|
|
|
// then merge with empty lines between.
|
|
|
|
//
|
|
|
|
// What is under-defined in the documentation is what fill() considers a "paragraph".
|
|
|
|
// From testing and reviewing the rather obtuse code, a paragraph is any number of lines
|
|
|
|
// that start with a line that has optional whitespace at its start, then all subsequent lines
|
|
|
|
// that have no whitespace at the start, until either reaching another line that has leading
|
|
|
|
// whitespace, or a blank line. fill() also "destroy[s] any whitespace in the original text",
|
|
|
|
// by which it appearently means all tabs and multiple-spaces are replaced with single-spaces.
|
|
|
|
//
|
|
|
|
// So, for example, `text\ntext` is one paragraph, `text\n text` is two paragraphs. Since for
|
|
|
|
// our purposes we don't want any indentation of paragraphs, the output of these two examples
|
|
|
|
// should be `text text` and `text\n\ntext`, respectively.
|
|
|
|
//
|
|
|
|
// This is a pretty gnarly! Might be easier in a multi-pass model: collect together each paragraph,
|
|
|
|
// collapse them into single strings and format in a sub-function, then merge them all together.
|
|
|
|
// That's closer to how the perl module does it.
|
2024-03-28 01:41:34 +00:00
|
|
|
func wordWrap(text []string, column int) ([]string, int) {
|
|
|
|
out := []string{}
|
|
|
|
length := 0
|
|
|
|
var b strings.Builder
|
2024-03-31 00:10:41 +00:00
|
|
|
var inParagraph bool
|
|
|
|
for beginning, line := range text {
|
2024-03-28 01:41:34 +00:00
|
|
|
// remove control character whitespace
|
|
|
|
line = strings.ReplaceAll(line, "\t", " ")
|
|
|
|
line = strings.ReplaceAll(line, "\v", " ")
|
|
|
|
line = strings.ReplaceAll(line, "\f", " ")
|
2024-03-31 00:10:41 +00:00
|
|
|
line = strings.ReplaceAll(line, "\r", " ")
|
2024-03-28 01:41:34 +00:00
|
|
|
words := strings.Split(line, " ")
|
2024-03-31 00:10:41 +00:00
|
|
|
// skip empty newlines if not in a paragraph, but start a new paragraph if we are.
|
|
|
|
if strings.TrimSpace(line) == "" {
|
|
|
|
if inParagraph {
|
|
|
|
length = max(length, b.Len()-1)
|
|
|
|
out = append(out, b.String()[0:b.Len()-1])
|
|
|
|
out = append(out, "")
|
|
|
|
b = strings.Builder{}
|
|
|
|
inParagraph = false
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
2024-03-28 01:41:34 +00:00
|
|
|
// we -1 all over the place to clean up the loose space on the end of the builder
|
2024-03-31 00:10:41 +00:00
|
|
|
for i, word := range words { // split into words
|
|
|
|
if DEBUG {
|
|
|
|
log.Printf("%d, `%s`", i, b.String())
|
|
|
|
}
|
|
|
|
if inParagraph && i == 0 && word == "" && len(words) != 0 {
|
|
|
|
// we've found a new paragraph while we were still processing the old one.
|
|
|
|
// (that is, the new line we're parsing had leading whitespace)
|
|
|
|
length = max(length, b.Len()-1)
|
|
|
|
out = append(out, b.String()[0:b.Len()-1])
|
|
|
|
out = append(out, "")
|
|
|
|
b = strings.Builder{}
|
|
|
|
log.Println("here")
|
|
|
|
}
|
|
|
|
// bizarrely, cowsay allows for indentation to survive in the /first/ paragraph,
|
|
|
|
// but only up to two spaces worth.
|
|
|
|
if beginning == 0 && (b.Len() == 0 || b.String() == " ") && word == "" {
|
|
|
|
b.WriteRune(' ')
|
|
|
|
}
|
|
|
|
if b.Len() == 0 && word != "" {
|
|
|
|
inParagraph = true
|
|
|
|
}
|
|
|
|
if b.Len() >= 0 && word == "" && i+1 != len(words) {
|
|
|
|
// skip multiple spaces in a row...
|
|
|
|
// ...but a single trailing space on a line is OK??
|
2024-03-28 01:41:34 +00:00
|
|
|
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{}
|
|
|
|
}
|
2024-03-31 00:10:41 +00:00
|
|
|
for b.Len() == 0 && len(word) >= column {
|
2024-03-28 01:41:34 +00:00
|
|
|
// our word is longer than our maximum column size. let's break it up.
|
2024-03-31 00:10:41 +00:00
|
|
|
// we loop until we've consumed the full overly-long word.
|
2024-03-28 01:41:34 +00:00
|
|
|
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(' ')
|
2024-03-31 00:10:41 +00:00
|
|
|
if DEBUG {
|
|
|
|
log.Printf("%d, `%s`", i, b.String())
|
|
|
|
}
|
2024-03-28 01:41:34 +00:00
|
|
|
}
|
|
|
|
}
|
2024-03-31 00:10:41 +00:00
|
|
|
// out of words! save off our last line.
|
|
|
|
length = max(length, b.Len()-1)
|
|
|
|
out = append(out, b.String()[0:b.Len()-1])
|
2024-03-28 01:41:34 +00:00
|
|
|
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()
|
|
|
|
}
|