cowsay.gno
3.15 Kb · 132 lines
1// Package cowsay is an on-chain Gno port of the classic `cowsay` program:
2// it builds an ASCII cow with a speech bubble containing a message — as a
3// reusable pure package.
4//
5// Original: cowsay by Tony Monroe (1999), a Perl program later ported to Go
6// many times. This package reimplements the core rendering — bubble framing,
7// word-wrapping and the cow art — as pure, deterministic string logic.
8//
9// A live demo of this package (an interactive cow that says whatever you put in
10// the path) is at [r/moul/x/daily/cowsaydemo](/r/moul/x/daily/cowsaydemo/v0).
11package cowsay
12
13import (
14 "strings"
15)
16
17// defaultMessage is shown when no message is supplied.
18const defaultMessage = "Moo!"
19
20// wrapWidth is the maximum bubble text width (classic cowsay uses 40).
21const wrapWidth = 40
22
23// Say returns the full cowsay art (speech bubble + cow) for msg.
24// An empty msg falls back to the default message.
25func Say(msg string) string {
26 msg = strings.TrimSpace(msg)
27 if msg == "" {
28 msg = defaultMessage
29 }
30 lines := wrap(msg, wrapWidth)
31 return balloon(lines) + cow
32}
33
34// balloon builds the speech bubble around the given (already wrapped) lines.
35func balloon(lines []string) string {
36 width := maxLen(lines)
37
38 var b strings.Builder
39 // top border: " " + "_"*(width+2)
40 b.WriteString(" ")
41 b.WriteString(strings.Repeat("_", width+2))
42 b.WriteString("\n")
43
44 n := len(lines)
45 for i, ln := range lines {
46 left, right := borderChars(i, n)
47 b.WriteString(left)
48 b.WriteString(" ")
49 b.WriteString(ln)
50 b.WriteString(strings.Repeat(" ", width-len(ln)))
51 b.WriteString(" ")
52 b.WriteString(right)
53 b.WriteString("\n")
54 }
55
56 // bottom border: " " + "-"*(width+2)
57 b.WriteString(" ")
58 b.WriteString(strings.Repeat("-", width+2))
59 b.WriteString("\n")
60 return b.String()
61}
62
63// borderChars picks the left/right bubble characters for line i of n.
64// Single line uses < >. Multi-line uses / \ for the first row,
65// \ / for the last row, and | | for the middle rows.
66func borderChars(i, n int) (string, string) {
67 if n == 1 {
68 return "<", ">"
69 }
70 switch i {
71 case 0:
72 return "/", "\\"
73 case n - 1:
74 return "\\", "/"
75 default:
76 return "|", "|"
77 }
78}
79
80// wrap splits text into lines no longer than width, breaking on spaces.
81// Words longer than width are hard-split.
82func wrap(text string, width int) []string {
83 words := strings.Fields(text)
84 if len(words) == 0 {
85 return []string{""}
86 }
87
88 var lines []string
89 cur := ""
90 for _, w := range words {
91 // hard-split a single word that is too long
92 for len(w) > width {
93 if cur != "" {
94 lines = append(lines, cur)
95 cur = ""
96 }
97 lines = append(lines, w[:width])
98 w = w[width:]
99 }
100 if cur == "" {
101 cur = w
102 } else if len(cur)+1+len(w) <= width {
103 cur = cur + " " + w
104 } else {
105 lines = append(lines, cur)
106 cur = w
107 }
108 }
109 if cur != "" {
110 lines = append(lines, cur)
111 }
112 return lines
113}
114
115// maxLen returns the length of the longest line.
116func maxLen(lines []string) int {
117 m := 0
118 for _, ln := range lines {
119 if len(ln) > m {
120 m = len(ln)
121 }
122 }
123 return m
124}
125
126// cow is the classic happy cow, aligned under the bubble.
127const cow = ` \ ^__^
128 \ (oo)\_______
129 (__)\ )\/\
130 ||----w |
131 || ||
132`