├── .github ├── CODEOWNERS ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── lgtm.yml │ ├── main.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── balloon.go ├── bin └── .gitkeep ├── cmd ├── cmd.go ├── cowsay │ └── main.go ├── cowthink │ └── main.go ├── go.mod ├── go.sum ├── internal │ ├── cli │ │ ├── cli.go │ │ └── cli_test.go │ ├── screen │ │ ├── buffer.go │ │ └── screen.go │ └── super │ │ └── supercow.go └── testdata │ ├── cowsay │ ├── W_option.txt │ ├── b_option.txt │ ├── d_option.txt │ ├── eyes_option.txt │ ├── f_tux_option.txt │ ├── g_option.txt │ ├── n_option.txt │ ├── p_option.txt │ ├── s_option.txt │ ├── t_option.txt │ ├── tongue_option.txt │ ├── wired_option.txt │ └── y_option.txt │ └── cowthink │ ├── W_option.txt │ ├── b_option.txt │ ├── d_option.txt │ ├── eyes_option.txt │ ├── f_tux_option.txt │ ├── g_option.txt │ ├── n_option.txt │ ├── p_option.txt │ ├── s_option.txt │ ├── t_option.txt │ ├── tongue_option.txt │ ├── wired_option.txt │ └── y_option.txt ├── cow.go ├── cow_test.go ├── cows ├── beavis.zen.cow ├── bong.cow ├── bud-frogs.cow ├── bunny.cow ├── cheese.cow ├── cower.cow ├── daemon.cow ├── default.cow ├── deno.cow ├── docker.cow ├── dragon-and-cow.cow ├── dragon.cow ├── elephant-in-snake.cow ├── elephant.cow ├── eyes.cow ├── flaming-sheep.cow ├── ghostbusters.cow ├── gopher.cow ├── head-in.cow ├── hellokitty.cow ├── kiss.cow ├── kitty.cow ├── koala.cow ├── kosh.cow ├── luke-koala.cow ├── meow.cow ├── milk.cow ├── moofasa.cow ├── moose.cow ├── mutilated.cow ├── ren.cow ├── sage.cow ├── satanic.cow ├── sheep.cow ├── skeleton.cow ├── small.cow ├── sodomized.cow ├── squirrel.cow ├── stegosaurus.cow ├── stimpy.cow ├── supermilker.cow ├── surgery.cow ├── telebears.cow ├── turkey.cow ├── turtle.cow ├── tux.cow ├── vader-koala.cow ├── vader.cow └── www.cow ├── cowsay.go ├── cowsay_test.go ├── decoration ├── aurora.go ├── bold.go ├── decoration.go └── rainbow.go ├── doc ├── cowsay.1 └── cowsay.1.txt.tpl ├── embed.go ├── examples ├── basic │ ├── go.mod │ ├── go.sum │ └── main.go └── echo-server │ ├── README.md │ ├── go.mod │ ├── go.sum │ └── main.go ├── go.mod ├── go.sum ├── split.go ├── split_windows.go └── testdata ├── default.cow ├── nest.cow └── testdir └── test.cow /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Code-Hex -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: Code-Hex 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "gomod" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" 11 | -------------------------------------------------------------------------------- /.github/workflows/lgtm.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request_review: 3 | types: 4 | - submitted 5 | 6 | jobs: 7 | lgtm: 8 | name: LGTM 9 | runs-on: ubuntu-latest 10 | if: github.event.review.state == 'approved' 11 | steps: 12 | - name: Cowsay LGTM 13 | uses: Code-Hex/neo-cowsay-action@v1.0.2 14 | with: 15 | message: 'LGTM' 16 | cow: 'random' 17 | cowsay_on_comment: true 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - "master" 5 | pull_request: 6 | 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | os: [ubuntu-latest, windows-latest] 12 | name: test 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - name: Setup Go 1.17.2 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: 1.17.2 19 | - name: Check out code into the Go module directory 20 | uses: actions/checkout@v3 21 | - name: Vet 22 | run: make vet 23 | - name: Test 24 | run: make test 25 | - name: Lint 26 | if: matrix.os == 'ubuntu-latest' 27 | run: | 28 | go get golang.org/x/lint/golint 29 | export PATH="$PATH:$(go env GOPATH)/bin" 30 | make lint 31 | env: 32 | GO111MODULE: off 33 | - name: Declare some variables 34 | if: matrix.os == 'ubuntu-latest' 35 | id: vars 36 | run: | 37 | echo "::set-output name=coverage_txt::${RUNNER_TEMP}/coverage.txt" 38 | - name: Test Coverage (pkg) 39 | if: matrix.os == 'ubuntu-latest' 40 | run: go test ./... -coverprofile=${{ steps.vars.outputs.coverage_txt }} 41 | - name: Upload coverage 42 | if: matrix.os == 'ubuntu-latest' 43 | uses: codecov/codecov-action@v3 44 | with: 45 | files: ${{ steps.vars.outputs.coverage_txt }} -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "v*.*.*" 5 | 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Setup Go 1.17.2 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: 1.17.2 15 | - name: Check out code into the Go module directory 16 | uses: actions/checkout@v3 17 | - name: Run GoReleaser 18 | if: contains(github.ref, 'tags/v') 19 | uses: goreleaser/goreleaser-action@v2 20 | with: 21 | version: latest 22 | args: release --rm-dist 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} 26 | 27 | winget: 28 | runs-on: ubuntu-latest 29 | needs: release 30 | steps: 31 | - uses: vedantmgoyal2009/winget-releaser@v2 32 | with: 33 | identifier: codehex.Neo-cowsay 34 | installers-regex: '_Windows_\w+\.zip$' 35 | token: ${{ secrets.WINGET_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | github-token 27 | build 28 | releases 29 | bin/* 30 | !bin/.gitkeep 31 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - id: cowsay 3 | dir: cmd 4 | main: ./cowsay/main.go 5 | binary: cowsay 6 | env: 7 | - CGO_ENABLED=0 8 | ldflags: -s -w -X main.version={{.Version}} 9 | goos: 10 | - linux 11 | - darwin 12 | - windows 13 | goarch: 14 | - 386 15 | - amd64 16 | - arm 17 | - arm64 18 | goarm: 19 | - 6 20 | - 7 21 | ignore: 22 | - goos: darwin 23 | goarch: 386 24 | - goos: linux 25 | goarch: arm 26 | goarm: 7 27 | - goos: windows 28 | goarch: arm 29 | goarm: 7 30 | - id: cowthink 31 | dir: cmd 32 | main: ./cowthink/main.go 33 | binary: cowthink 34 | env: 35 | - CGO_ENABLED=0 36 | ldflags: -s -w -X main.version={{.Version}} 37 | goos: 38 | - linux 39 | - darwin 40 | - windows 41 | goarch: 42 | - 386 43 | - amd64 44 | - arm 45 | - arm64 46 | goarm: 47 | - 6 48 | - 7 49 | ignore: 50 | - goos: darwin 51 | goarch: 386 52 | - goos: linux 53 | goarch: arm 54 | goarm: 7 55 | - goos: windows 56 | goarch: arm 57 | goarm: 7 58 | 59 | archives: 60 | - builds: 61 | - cowsay 62 | - cowthink 63 | name_template: 'cowsay_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 64 | replacements: 65 | darwin: macOS 66 | linux: Linux 67 | windows: Windows 68 | 386: i386 69 | amd64: x86_64 70 | format_overrides: 71 | - goos: windows 72 | format: zip 73 | files: 74 | - LICENSE 75 | - doc/cowsay.1 76 | 77 | brews: 78 | - name: neo-cowsay 79 | tap: 80 | owner: Code-Hex 81 | name: homebrew-tap 82 | token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" 83 | homepage: https://github.com/Code-Hex/Neo-cowsay 84 | description: "Fast, funny, everyone wanted? new cowsay!!" 85 | folder: Formula 86 | install: | 87 | bin.install "cowsay" 88 | bin.install "cowthink" 89 | man1.install Dir["doc/cowsay.1"] 90 | 91 | nfpms: 92 | - license: Artistic License 2.0 93 | maintainer: Kei Kamikawa 94 | homepage: https://github.com/Code-Hex/Neo-cowsay 95 | bindir: /usr/local/bin 96 | description: "Fast, funny, everyone wanted? new cowsay!!" 97 | formats: 98 | - apk 99 | - deb 100 | - rpm 101 | contents: 102 | - src: "doc/cowsay.1" 103 | dst: "/usr/share/man/man1/cowsay.1" 104 | 105 | checksum: 106 | name_template: 'cowsay_checksums.txt' 107 | 108 | changelog: 109 | sort: asc 110 | filters: 111 | exclude: 112 | - '^docs:' 113 | - '^test:' 114 | - Merge pull request 115 | - Merge branch 116 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The Artistic License 2.0 2 | 3 | Copyright (c) 2000-2006, The Perl Foundation. 4 | 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | This license establishes the terms under which a given free software 11 | Package may be copied, modified, distributed, and/or redistributed. 12 | The intent is that the Copyright Holder maintains some artistic 13 | control over the development of that Package while still keeping the 14 | Package available as open source and free software. 15 | 16 | You are always permitted to make arrangements wholly outside of this 17 | license directly with the Copyright Holder of a given Package. If the 18 | terms of this license do not permit the full use that you propose to 19 | make of the Package, you should contact the Copyright Holder and seek 20 | a different licensing arrangement. 21 | 22 | Definitions 23 | 24 | "Copyright Holder" means the individual(s) or organization(s) 25 | named in the copyright notice for the entire Package. 26 | 27 | "Contributor" means any party that has contributed code or other 28 | material to the Package, in accordance with the Copyright Holder's 29 | procedures. 30 | 31 | "You" and "your" means any person who would like to copy, 32 | distribute, or modify the Package. 33 | 34 | "Package" means the collection of files distributed by the 35 | Copyright Holder, and derivatives of that collection and/or of 36 | those files. A given Package may consist of either the Standard 37 | Version, or a Modified Version. 38 | 39 | "Distribute" means providing a copy of the Package or making it 40 | accessible to anyone else, or in the case of a company or 41 | organization, to others outside of your company or organization. 42 | 43 | "Distributor Fee" means any fee that you charge for Distributing 44 | this Package or providing support for this Package to another 45 | party. It does not mean licensing fees. 46 | 47 | "Standard Version" refers to the Package if it has not been 48 | modified, or has been modified only in ways explicitly requested 49 | by the Copyright Holder. 50 | 51 | "Modified Version" means the Package, if it has been changed, and 52 | such changes were not explicitly requested by the Copyright 53 | Holder. 54 | 55 | "Original License" means this Artistic License as Distributed with 56 | the Standard Version of the Package, in its current version or as 57 | it may be modified by The Perl Foundation in the future. 58 | 59 | "Source" form means the source code, documentation source, and 60 | configuration files for the Package. 61 | 62 | "Compiled" form means the compiled bytecode, object code, binary, 63 | or any other form resulting from mechanical transformation or 64 | translation of the Source form. 65 | 66 | 67 | Permission for Use and Modification Without Distribution 68 | 69 | (1) You are permitted to use the Standard Version and create and use 70 | Modified Versions for any purpose without restriction, provided that 71 | you do not Distribute the Modified Version. 72 | 73 | 74 | Permissions for Redistribution of the Standard Version 75 | 76 | (2) You may Distribute verbatim copies of the Source form of the 77 | Standard Version of this Package in any medium without restriction, 78 | either gratis or for a Distributor Fee, provided that you duplicate 79 | all of the original copyright notices and associated disclaimers. At 80 | your discretion, such verbatim copies may or may not include a 81 | Compiled form of the Package. 82 | 83 | (3) You may apply any bug fixes, portability changes, and other 84 | modifications made available from the Copyright Holder. The resulting 85 | Package will still be considered the Standard Version, and as such 86 | will be subject to the Original License. 87 | 88 | 89 | Distribution of Modified Versions of the Package as Source 90 | 91 | (4) You may Distribute your Modified Version as Source (either gratis 92 | or for a Distributor Fee, and with or without a Compiled form of the 93 | Modified Version) provided that you clearly document how it differs 94 | from the Standard Version, including, but not limited to, documenting 95 | any non-standard features, executables, or modules, and provided that 96 | you do at least ONE of the following: 97 | 98 | (a) make the Modified Version available to the Copyright Holder 99 | of the Standard Version, under the Original License, so that the 100 | Copyright Holder may include your modifications in the Standard 101 | Version. 102 | 103 | (b) ensure that installation of your Modified Version does not 104 | prevent the user installing or running the Standard Version. In 105 | addition, the Modified Version must bear a name that is different 106 | from the name of the Standard Version. 107 | 108 | (c) allow anyone who receives a copy of the Modified Version to 109 | make the Source form of the Modified Version available to others 110 | under 111 | 112 | (i) the Original License or 113 | 114 | (ii) a license that permits the licensee to freely copy, 115 | modify and redistribute the Modified Version using the same 116 | licensing terms that apply to the copy that the licensee 117 | received, and requires that the Source form of the Modified 118 | Version, and of any works derived from it, be made freely 119 | available in that license fees are prohibited but Distributor 120 | Fees are allowed. 121 | 122 | 123 | Distribution of Compiled Forms of the Standard Version 124 | or Modified Versions without the Source 125 | 126 | (5) You may Distribute Compiled forms of the Standard Version without 127 | the Source, provided that you include complete instructions on how to 128 | get the Source of the Standard Version. Such instructions must be 129 | valid at the time of your distribution. If these instructions, at any 130 | time while you are carrying out such distribution, become invalid, you 131 | must provide new instructions on demand or cease further distribution. 132 | If you provide valid instructions or cease distribution within thirty 133 | days after you become aware that the instructions are invalid, then 134 | you do not forfeit any of your rights under this license. 135 | 136 | (6) You may Distribute a Modified Version in Compiled form without 137 | the Source, provided that you comply with Section 4 with respect to 138 | the Source of the Modified Version. 139 | 140 | 141 | Aggregating or Linking the Package 142 | 143 | (7) You may aggregate the Package (either the Standard Version or 144 | Modified Version) with other packages and Distribute the resulting 145 | aggregation provided that you do not charge a licensing fee for the 146 | Package. Distributor Fees are permitted, and licensing fees for other 147 | components in the aggregation are permitted. The terms of this license 148 | apply to the use and Distribution of the Standard or Modified Versions 149 | as included in the aggregation. 150 | 151 | (8) You are permitted to link Modified and Standard Versions with 152 | other works, to embed the Package in a larger work of your own, or to 153 | build stand-alone binary or bytecode versions of applications that 154 | include the Package, and Distribute the result without restriction, 155 | provided the result does not expose a direct interface to the Package. 156 | 157 | 158 | Items That are Not Considered Part of a Modified Version 159 | 160 | (9) Works (including, but not limited to, modules and scripts) that 161 | merely extend or make use of the Package, do not, by themselves, cause 162 | the Package to be a Modified Version. In addition, such works are not 163 | considered parts of the Package itself, and are not subject to the 164 | terms of this license. 165 | 166 | 167 | General Provisions 168 | 169 | (10) Any use, modification, and distribution of the Standard or 170 | Modified Versions is governed by this Artistic License. By using, 171 | modifying or distributing the Package, you accept this license. Do not 172 | use, modify, or distribute the Package, if you do not accept this 173 | license. 174 | 175 | (11) If your Modified Version has been derived from a Modified 176 | Version made by someone other than you, you are nevertheless required 177 | to ensure that your Modified Version complies with the requirements of 178 | this license. 179 | 180 | (12) This license does not grant you the right to use any trademark, 181 | service mark, tradename, or logo of the Copyright Holder. 182 | 183 | (13) This license includes the non-exclusive, worldwide, 184 | free-of-charge patent license to make, have made, use, offer to sell, 185 | sell, import and otherwise transfer the Package with respect to any 186 | patent claims licensable by the Copyright Holder that are necessarily 187 | infringed by the Package. If you institute patent litigation 188 | (including a cross-claim or counterclaim) against any party alleging 189 | that the Package constitutes direct or contributory patent 190 | infringement, then this Artistic License to you shall terminate on the 191 | date that such litigation is filed. 192 | 193 | (14) Disclaimer of Warranty: 194 | THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS 195 | IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED 196 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR 197 | NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL 198 | LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL 199 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 200 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF 201 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: build/cowsay build/cowthink 3 | 4 | .PHONY: build/cowsay 5 | build/cowsay: 6 | CGO_ENABLED=0 cd cmd && go build -o ../bin/cowsay -ldflags "-w -s" ./cowsay 7 | 8 | .PHONY: build/cowthink 9 | build/cowthink: 10 | CGO_ENABLED=0 cd cmd && go build -o ../bin/cowthink -ldflags "-w -s" ./cowthink 11 | 12 | .PHONY: lint 13 | lint: 14 | golint ./... 15 | cd cmd && golint ./... 16 | 17 | .PHONY: vet 18 | vet: 19 | go vet ./... 20 | cd cmd && go vet ./... 21 | 22 | .PHONY: test 23 | test: test/pkg test/cli 24 | 25 | .PHONY: test/pkg 26 | test/pkg: 27 | go test ./... 28 | 29 | .PHONY: test/cli 30 | test/cli: 31 | cd cmd && go test ./... 32 | 33 | .PHONY: man 34 | man: 35 | asciidoctor --doctype manpage --backend manpage doc/cowsay.1.txt.tpl -o doc/cowsay.1 36 | 37 | .PHONY: man/preview 38 | man/preview: 39 | cat doc/cowsay.1 | groff -man -Tascii | less 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Neo Cowsay 2 | 3 | Neo Cowsay is written in Go. This cowsay is extended the original cowsay. added fun more options, and you can be used as a library. 4 | 5 | for GitHub Actions users: [Code-Hex/neo-cowsay-action](https://github.com/marketplace/actions/neo-cowsay) 6 | 7 | [![Go Reference](https://pkg.go.dev/badge/github.com/Code-Hex/Neo-cowsay/v2.svg)](https://pkg.go.dev/github.com/Code-Hex/Neo-cowsay/v2) [![.github/workflows/main.yml](https://github.com/Code-Hex/Neo-cowsay/actions/workflows/main.yml/badge.svg)](https://github.com/Code-Hex/Neo-cowsay/actions/workflows/main.yml) [![Go Report Card](https://goreportcard.com/badge/github.com/Code-Hex/Neo-cowsay)](https://goreportcard.com/report/github.com/Code-Hex/Neo-cowsay) [![codecov](https://codecov.io/gh/Code-Hex/Neo-cowsay/branch/master/graph/badge.svg?token=WwjmyHrOPv)](https://codecov.io/gh/Code-Hex/Neo-cowsay) 8 | 9 | ``` 10 | ______________ 11 | < I'm Neo cows > 12 | -------------- 13 | \ ^__^ 14 | \ (oo)\_______ 15 | (__)\ )\/\ 16 | ||----w | 17 | || || 18 | ``` 19 | 20 | ## About cowsay 21 | 22 | According to the [original](https://web.archive.org/web/20071026043648/http://www.nog.net/~tony/warez/cowsay.shtml) original manual. 23 | 24 | ``` 25 | cowsay is a configurable talking cow, written in Perl. It operates 26 | much as the figlet program does, and it written in the same spirit 27 | of silliness. 28 | ``` 29 | 30 | This is also supported `COWPATH` env. Please read more details in [#33](https://github.com/Code-Hex/Neo-cowsay/pull/33) if you want to use this. 31 | 32 | ## What makes it different from the original? 33 | 34 | - fast 35 | - utf8 is supported 36 | - new some cowfiles is added 37 | - cowfiles in binary 38 | - random pickup cowfile option 39 | - provides command-line fuzzy finder to search any cows with `-f -` [#39](https://github.com/Code-Hex/Neo-cowsay/pull/39) 40 | - coloring filter options 41 | - super mode 42 | 43 |
44 | Movies for new options 🐮 45 | 46 | ### Random 47 | 48 | [![asciicast](https://asciinema.org/a/228210.svg)](https://asciinema.org/a/228210) 49 | 50 | ### Rainbow and Aurora, Bold 51 | 52 | [![asciicast](https://asciinema.org/a/228213.svg)](https://asciinema.org/a/228213) 53 | 54 | ## And, Super Cows mode 55 | 56 | https://user-images.githubusercontent.com/6500104/140379043-53e44994-b1b0-442e-bda7-4f7ab3aedf01.mov 57 | 58 |
59 | 60 | ## Usage 61 | 62 | ### As command 63 | 64 | ``` 65 | cow{say,think} version 2.0.0, (c) 2021 codehex 66 | Usage: cowsay [-bdgpstwy] [-h] [-e eyes] [-f cowfile] [--random] 67 | [-l] [-n] [-T tongue] [-W wrapcolumn] 68 | [--bold] [--rainbow] [--aurora] [--super] [message] 69 | 70 | Original Author: (c) 1999 Tony Monroe 71 | Repository: https://github.com/Code-Hex/Neo-cowsay 72 | ``` 73 | Normal 74 | ``` 75 | $ cowsay Hello 76 | _______ 77 | < Hello > 78 | ------- 79 | \ ^__^ 80 | \ (oo)\_______ 81 | (__)\ )\/\ 82 | ||----w | 83 | || || 84 | ``` 85 | Borg mode 86 | ``` 87 | $ cowsay -b Hello 88 | _______ 89 | < Hello > 90 | ------- 91 | \ ^__^ 92 | \ (==)\_______ 93 | (__)\ )\/\ 94 | ||----w | 95 | || || 96 | ``` 97 | 98 | ### As library 99 | 100 | ```go 101 | package main 102 | 103 | import ( 104 | "fmt" 105 | 106 | cowsay "github.com/Code-Hex/Neo-cowsay/v2" 107 | ) 108 | 109 | func main() { 110 | say, err := cowsay.Say( 111 | "Hello", 112 | cowsay.Type("default"), 113 | cowsay.BallonWidth(40), 114 | ) 115 | if err != nil { 116 | panic(err) 117 | } 118 | fmt.Println(say) 119 | } 120 | ``` 121 | 122 | [Examples](https://github.com/Code-Hex/Neo-cowsay/blob/master/examples) or [GoDoc](https://pkg.go.dev/github.com/Code-Hex/Neo-cowsay/v2) 123 | 124 | ## Install 125 | 126 | ### Windows users via Scoop 127 | 128 | $ scoop install neo-cowsay 129 | 130 | ### Windows users via Winget 131 | 132 | $ winget install neo-cowsay 133 | 134 | ### Mac and Linux users via Homebrew 135 | 136 | $ brew update 137 | $ brew install Code-Hex/tap/neo-cowsay 138 | 139 | ### Binary 140 | 141 | You can download from [here](https://github.com/Code-Hex/Neo-cowsay/releases) 142 | 143 | ### library 144 | 145 | $ go get github.com/Code-Hex/Neo-cowsay/v2 146 | 147 | ### Go 148 | 149 | #### cowsay 150 | 151 | $ go install github.com/Code-Hex/Neo-cowsay/cmd/v2/cowsay@latest 152 | 153 | #### cowthink 154 | 155 | $ go install github.com/Code-Hex/Neo-cowsay/cmd/v2/cowthink@latest 156 | 157 | ## License 158 | 159 |
160 | cowsay license 161 | 162 | ``` 163 | ============== 164 | cowsay License 165 | ============== 166 | 167 | cowsay is distributed under the same licensing terms as Perl: the 168 | Artistic License or the GNU General Public License. If you don't 169 | want to track down these licenses and read them for yourself, use 170 | the parts that I'd prefer: 171 | 172 | (0) I wrote it and you didn't. 173 | 174 | (1) Give credit where credit is due if you borrow the code for some 175 | other purpose. 176 | 177 | (2) If you have any bugfixes or suggestions, please notify me so 178 | that I may incorporate them. 179 | 180 | (3) If you try to make money off of cowsay, you suck. 181 | 182 | =============== 183 | cowsay Legalese 184 | =============== 185 | 186 | (0) Copyright (c) 1999 Tony Monroe. All rights reserved. All 187 | lefts may or may not be reversed at my discretion. 188 | 189 | (1) This software package can be freely redistributed or modified 190 | under the terms described above in the "cowsay License" section 191 | of this file. 192 | 193 | (2) cowsay is provided "as is," with no warranties whatsoever, 194 | expressed or implied. If you want some implied warranty about 195 | merchantability and/or fitness for a particular purpose, you will 196 | not find it here, because there is no such thing here. 197 | 198 | (3) I hate legalese. 199 | ``` 200 | 201 |
202 | 203 | (The Artistic License or The GNU General Public License) 204 | 205 | ## Author 206 | Neo Cowsay: [codehex](https://twitter.com/CodeHex) 207 | Original: (c) 1999 Tony Monroe 208 | -------------------------------------------------------------------------------- /balloon.go: -------------------------------------------------------------------------------- 1 | package cowsay 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | wordwrap "github.com/Code-Hex/go-wordwrap" 8 | runewidth "github.com/mattn/go-runewidth" 9 | ) 10 | 11 | type border struct { 12 | first [2]rune 13 | middle [2]rune 14 | last [2]rune 15 | only [2]rune 16 | } 17 | 18 | func (cow *Cow) borderType() border { 19 | if cow.thinking { 20 | return border{ 21 | first: [2]rune{'(', ')'}, 22 | middle: [2]rune{'(', ')'}, 23 | last: [2]rune{'(', ')'}, 24 | only: [2]rune{'(', ')'}, 25 | } 26 | } 27 | 28 | return border{ 29 | first: [2]rune{'/', '\\'}, 30 | middle: [2]rune{'|', '|'}, 31 | last: [2]rune{'\\', '/'}, 32 | only: [2]rune{'<', '>'}, 33 | } 34 | } 35 | 36 | type line struct { 37 | text string 38 | runeWidth int 39 | } 40 | 41 | type lines []*line 42 | 43 | func (cow *Cow) maxLineWidth(lines []*line) int { 44 | maxWidth := 0 45 | for _, line := range lines { 46 | if line.runeWidth > maxWidth { 47 | maxWidth = line.runeWidth 48 | } 49 | if !cow.disableWordWrap && maxWidth > cow.ballonWidth { 50 | return cow.ballonWidth 51 | } 52 | } 53 | return maxWidth 54 | } 55 | 56 | func (cow *Cow) getLines(phrase string) []*line { 57 | text := cow.canonicalizePhrase(phrase) 58 | lineTexts := strings.Split(text, "\n") 59 | lines := make([]*line, 0, len(lineTexts)) 60 | for _, lineText := range lineTexts { 61 | lines = append(lines, &line{ 62 | text: lineText, 63 | runeWidth: runewidth.StringWidth(lineText), 64 | }) 65 | } 66 | return lines 67 | } 68 | 69 | func (cow *Cow) canonicalizePhrase(phrase string) string { 70 | // Replace tab to 8 spaces 71 | phrase = strings.Replace(phrase, "\t", " ", -1) 72 | 73 | if cow.disableWordWrap { 74 | return phrase 75 | } 76 | width := cow.ballonWidth 77 | return wordwrap.WrapString(phrase, uint(width)) 78 | } 79 | 80 | // Balloon to get the balloon and the string entered in the balloon. 81 | func (cow *Cow) Balloon(phrase string) string { 82 | defer cow.buf.Reset() 83 | 84 | lines := cow.getLines(phrase) 85 | maxWidth := cow.maxLineWidth(lines) 86 | 87 | cow.writeBallon(lines, maxWidth) 88 | 89 | return cow.buf.String() 90 | } 91 | 92 | func (cow *Cow) writeBallon(lines []*line, maxWidth int) { 93 | top := make([]byte, 0, maxWidth+2) 94 | bottom := make([]byte, 0, maxWidth+2) 95 | 96 | top = append(top, ' ') 97 | bottom = append(bottom, ' ') 98 | 99 | for i := 0; i < maxWidth+2; i++ { 100 | top = append(top, '_') 101 | bottom = append(bottom, '-') 102 | } 103 | 104 | borderType := cow.borderType() 105 | 106 | cow.buf.Write(top) 107 | cow.buf.Write([]byte{' ', '\n'}) 108 | defer func() { 109 | cow.buf.Write(bottom) 110 | cow.buf.Write([]byte{' ', '\n'}) 111 | }() 112 | 113 | l := len(lines) 114 | if l == 1 { 115 | border := borderType.only 116 | cow.buf.WriteRune(border[0]) 117 | cow.buf.WriteRune(' ') 118 | cow.buf.WriteString(lines[0].text) 119 | cow.buf.WriteRune(' ') 120 | cow.buf.WriteRune(border[1]) 121 | cow.buf.WriteRune('\n') 122 | return 123 | } 124 | 125 | var border [2]rune 126 | for i := 0; i < l; i++ { 127 | switch i { 128 | case 0: 129 | border = borderType.first 130 | case l - 1: 131 | border = borderType.last 132 | default: 133 | border = borderType.middle 134 | } 135 | cow.buf.WriteRune(border[0]) 136 | cow.buf.WriteRune(' ') 137 | cow.padding(lines[i], maxWidth) 138 | cow.buf.WriteRune(' ') 139 | cow.buf.WriteRune(border[1]) 140 | cow.buf.WriteRune('\n') 141 | } 142 | } 143 | 144 | func (cow *Cow) flush(text, top, bottom fmt.Stringer) string { 145 | return fmt.Sprintf( 146 | "%s\n%s%s\n", 147 | top.String(), 148 | text.String(), 149 | bottom.String(), 150 | ) 151 | } 152 | 153 | func (cow *Cow) padding(line *line, maxWidth int) { 154 | if maxWidth <= line.runeWidth { 155 | cow.buf.WriteString(line.text) 156 | return 157 | } 158 | 159 | cow.buf.WriteString(line.text) 160 | l := maxWidth - line.runeWidth 161 | for i := 0; i < l; i++ { 162 | cow.buf.WriteRune(' ') 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Code-Hex/Neo-cowsay/f68c20f068c26c55bc5a8572f1c582f0a1b08d34/bin/.gitkeep -------------------------------------------------------------------------------- /cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | -------------------------------------------------------------------------------- /cmd/cowsay/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/Code-Hex/Neo-cowsay/cmd/v2/internal/cli" 7 | ) 8 | 9 | var version string 10 | 11 | func main() { 12 | os.Exit((&cli.CLI{ 13 | Version: version, 14 | Thinking: false, 15 | }).Run(os.Args[1:])) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/cowthink/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/Code-Hex/Neo-cowsay/cmd/v2/internal/cli" 7 | ) 8 | 9 | var version string 10 | 11 | func main() { 12 | os.Exit((&cli.CLI{ 13 | Version: version, 14 | Thinking: true, 15 | }).Run(os.Args[1:])) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Code-Hex/Neo-cowsay/cmd/v2 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/Code-Hex/Neo-cowsay/v2 v2.0.3 7 | github.com/Code-Hex/go-wordwrap v1.0.0 8 | github.com/google/go-cmp v0.5.6 9 | github.com/jessevdk/go-flags v1.5.0 10 | github.com/ktr0731/go-fuzzyfinder v0.5.1 11 | github.com/mattn/go-colorable v0.1.11 12 | github.com/mattn/go-runewidth v0.0.13 13 | github.com/rivo/uniseg v0.2.0 14 | golang.org/x/crypto v0.1.0 15 | ) 16 | 17 | replace github.com/Code-Hex/Neo-cowsay/v2 => ../ 18 | -------------------------------------------------------------------------------- /cmd/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Code-Hex/go-wordwrap v1.0.0 h1:yl5fLyZEz3+hPGbpTRlTQ8mQJ1HXWcTq1FCNR1ch6zM= 2 | github.com/Code-Hex/go-wordwrap v1.0.0/go.mod h1:/SsbgkY2Q0aPQRyvXcyQwWYTQOIwSORKe6MPjRVGIWU= 3 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 4 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 5 | github.com/gdamore/tcell/v2 v2.4.0 h1:W6dxJEmaxYvhICFoTY3WrLLEXsQ11SaFnKGVEXW57KM= 6 | github.com/gdamore/tcell/v2 v2.4.0/go.mod h1:cTTuF84Dlj/RqmaCIV5p4w8uG1zWdk0SF6oBpwHp4fU= 7 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 8 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 9 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 10 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 11 | github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= 12 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 13 | github.com/ktr0731/go-fuzzyfinder v0.5.1 h1:rDcWxmGi6ux4NURekn9iAXpbYBp8Kj4cznrz162S9og= 14 | github.com/ktr0731/go-fuzzyfinder v0.5.1/go.mod h1:gud27uRG2vF+oD58eGhYZj7Pc9enRX0qecwp09w/jno= 15 | github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= 16 | github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 17 | github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs= 18 | github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 19 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 20 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 21 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 22 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 23 | github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 24 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 25 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 26 | github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00 h1:Rl8NelBe+n7SuLbJyw13ho7CGWUt2BjGGKIoreCWQ/c= 27 | github.com/nsf/termbox-go v0.0.0-20201124104050-ed494de23a00/go.mod h1:T0cTdVuOwf7pHQNtfhnEbzHbcNyCEcVU4YPpouCbVxo= 28 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 29 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 30 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 31 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 32 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 33 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 34 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 35 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 36 | golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= 37 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 38 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 39 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 40 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 41 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 42 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 43 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 44 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 45 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 46 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 47 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 48 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 49 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 50 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 51 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 52 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 53 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 54 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 55 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 56 | golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 57 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 58 | golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= 59 | golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 60 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 61 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 62 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 63 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 64 | golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= 65 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 66 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 67 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 68 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 69 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 70 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 71 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 72 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 73 | -------------------------------------------------------------------------------- /cmd/internal/cli/cli.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 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 | "github.com/Code-Hex/Neo-cowsay/cmd/v2/internal/super" 18 | cowsay "github.com/Code-Hex/Neo-cowsay/v2" 19 | "github.com/Code-Hex/Neo-cowsay/v2/decoration" 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 | 26 | func 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 41 | type 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. 65 | type CLI struct { 66 | Version string 67 | Thinking bool 68 | stderr io.Writer 69 | stdout io.Writer 70 | stdin io.Reader 71 | } 72 | 73 | func (c *CLI) program() string { 74 | if c.Thinking { 75 | return "cowthink" 76 | } 77 | return "cowsay" 78 | } 79 | 80 | // Run runs command-line. 81 | func (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. 99 | func (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 | 130 | func (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 | 145 | func (c *CLI) usage() []byte { 146 | year := strconv.Itoa(time.Now().Year()) 147 | return []byte(c.program() + ` version ` + c.Version + `, (c) ` + year + ` codehex 148 | Usage: ` + c.program() + ` [-bdgpstwy] [-h] [-e eyes] [-f cowfile] [--random] 149 | [-l] [-n] [-T tongue] [-W wrapcolumn] 150 | [--bold] [--rainbow] [--aurora] [--super] [message] 151 | 152 | Original Author: (c) 1999 Tony Monroe 153 | `) 154 | } 155 | 156 | func (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 | 190 | func 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 | 202 | func (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 | 214 | func (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, ¬found) { 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 | 248 | func 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 | -------------------------------------------------------------------------------- /cmd/internal/cli/cli_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "path/filepath" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | ) 13 | 14 | func TestCLI_Run(t *testing.T) { 15 | clis := []struct { 16 | name string 17 | thinking bool 18 | }{ 19 | { 20 | name: "cowsay", 21 | thinking: false, 22 | }, 23 | { 24 | name: "cowthink", 25 | thinking: true, 26 | }, 27 | } 28 | for _, cli := range clis { 29 | cli := cli 30 | t.Run(cli.name, func(t *testing.T) { 31 | t.Parallel() 32 | tests := []struct { 33 | name string 34 | phrase string 35 | argv []string 36 | testfile string 37 | }{ 38 | { 39 | name: "ignore wordwrap option", 40 | phrase: "foo\nbar\nbaz", 41 | argv: []string{"-n"}, 42 | testfile: "n_option.txt", 43 | }, 44 | { 45 | name: "tired option", 46 | phrase: "tired", 47 | argv: []string{"-t"}, 48 | testfile: "t_option.txt", 49 | }, 50 | { 51 | name: "specifies width of the ballon is 3", 52 | phrase: "foobarbaz", 53 | argv: []string{"-W", "3"}, 54 | testfile: "W_option.txt", 55 | }, 56 | { 57 | name: "borg mode", 58 | phrase: "foobarbaz", 59 | argv: []string{"-b"}, 60 | testfile: "b_option.txt", 61 | }, 62 | { 63 | name: "dead mode", 64 | phrase: "0xdeadbeef", 65 | argv: []string{"-d"}, 66 | testfile: "d_option.txt", 67 | }, 68 | { 69 | name: "greedy mode", 70 | phrase: "give me money", 71 | argv: []string{"-g"}, 72 | testfile: "g_option.txt", 73 | }, 74 | { 75 | name: "paranoid mode", 76 | phrase: "everyone hates me", 77 | argv: []string{"-p"}, 78 | testfile: "p_option.txt", 79 | }, 80 | { 81 | name: "stoned mode", 82 | phrase: "I don't know", 83 | argv: []string{"-s"}, 84 | testfile: "s_option.txt", 85 | }, 86 | { 87 | name: "wired mode", 88 | phrase: "Wanna Netflix and chill?", 89 | argv: []string{"-w"}, 90 | testfile: "wired_option.txt", 91 | }, 92 | { 93 | name: "youthful mode", 94 | phrase: "I forgot my ID at home", 95 | argv: []string{"-y"}, 96 | testfile: "y_option.txt", 97 | }, 98 | { 99 | name: "eyes option", 100 | phrase: "I'm not angry", 101 | argv: []string{"-e", "^^"}, 102 | testfile: "eyes_option.txt", 103 | }, 104 | { 105 | name: "tongue option", 106 | phrase: "hungry", 107 | argv: []string{"-T", ":"}, 108 | testfile: "tongue_option.txt", 109 | }, 110 | { 111 | name: "-f tux", 112 | phrase: "what is macOS?", 113 | argv: []string{"-f", "tux"}, 114 | testfile: "f_tux_option.txt", 115 | }, 116 | } 117 | for _, tt := range tests { 118 | tt := tt 119 | t.Run(tt.name, func(t *testing.T) { 120 | var stdout bytes.Buffer 121 | c := &CLI{ 122 | Thinking: cli.thinking, 123 | stdout: &stdout, 124 | stdin: strings.NewReader(tt.phrase), 125 | } 126 | exit := c.Run(tt.argv) 127 | if exit != 0 { 128 | t.Fatalf("unexpected exit code: %d", exit) 129 | } 130 | testpath := filepath.Join("..", "..", "testdata", cli.name, tt.testfile) 131 | content, err := ioutil.ReadFile(testpath) 132 | if err != nil { 133 | t.Fatal(err) 134 | } 135 | got := strings.Replace(stdout.String(), "\r", "", -1) // for windows 136 | want := strings.Replace(string(content), "\r", "", -1) // for windows 137 | if want != got { 138 | t.Log(cmp.Diff(want, got)) 139 | t.Errorf("want\n%s\n-----got\n%s\n", want, got) 140 | } 141 | }) 142 | } 143 | 144 | t.Run("program name", func(t *testing.T) { 145 | c := &CLI{Thinking: cli.thinking} 146 | if cli.name != c.program() { 147 | t.Fatalf("want %q, but got %q", cli.name, c.program()) 148 | } 149 | }) 150 | 151 | t.Run("not found cowfile", func(t *testing.T) { 152 | var stderr bytes.Buffer 153 | c := &CLI{ 154 | Thinking: cli.thinking, 155 | stderr: &stderr, 156 | } 157 | 158 | exit := c.Run([]string{"-f", "unknown"}) 159 | if exit == 0 { 160 | t.Errorf("unexpected exit code: %d", exit) 161 | } 162 | want := fmt.Sprintf("%s: could not find unknown cowfile\n", cli.name) 163 | if want != stderr.String() { 164 | t.Errorf("want %q, but got %q", want, stderr.String()) 165 | } 166 | }) 167 | }) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /cmd/internal/screen/buffer.go: -------------------------------------------------------------------------------- 1 | package screen 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "strings" 8 | ) 9 | 10 | // buffer is the global screen buffer 11 | // Its not recommended write to buffer dirrectly, use package Print,Printf,Println functions instead. 12 | var buffer strings.Builder 13 | 14 | // Flush buffer and ensure that it will not overflow screen 15 | func Flush() string { 16 | defer buffer.Reset() 17 | return buffer.String() 18 | } 19 | 20 | // MoveWriter is implemented io.Writer and io.StringWriter. 21 | type MoveWriter struct { 22 | idx int 23 | x, y int 24 | w io.Writer 25 | buf bytes.Buffer 26 | } 27 | 28 | var _ interface { 29 | io.Writer 30 | io.StringWriter 31 | } = (*MoveWriter)(nil) 32 | 33 | // NewMoveWriter creates a new MoveWriter. 34 | func NewMoveWriter(w io.Writer, x, y int) *MoveWriter { 35 | x, y = getXY(x, y) 36 | return &MoveWriter{ 37 | w: w, 38 | x: x, 39 | y: y, 40 | } 41 | } 42 | 43 | // SetPosx sets pos x 44 | func (m *MoveWriter) SetPosx(x int) { 45 | x, _ = getXY(x, 0) 46 | m.x = x 47 | } 48 | 49 | // Reset resets 50 | func (m *MoveWriter) Reset() { 51 | m.idx = 0 52 | m.buf.Reset() 53 | } 54 | 55 | // Write writes bytes. which is implemented io.Writer. 56 | func (m *MoveWriter) Write(bs []byte) (nn int, _ error) { 57 | br := bytes.NewReader(bs) 58 | for { 59 | b, err := br.ReadByte() 60 | if err != nil && err != io.EOF { 61 | return 0, err 62 | } 63 | if err == io.EOF { 64 | n, _ := fmt.Fprintf(m.w, "\x1b[%d;%dH%s\x1b[0K", 65 | m.y+m.idx, 66 | m.x, 67 | m.buf.String(), 68 | ) 69 | nn += n 70 | return 71 | } 72 | if b == '\n' { 73 | n, _ := fmt.Fprintf(m.w, "\x1b[%d;%dH%s\x1b[0K", 74 | m.y+m.idx, 75 | m.x, 76 | m.buf.String(), 77 | ) 78 | m.buf.Reset() 79 | m.idx++ 80 | nn += n 81 | } else { 82 | m.buf.WriteByte(b) 83 | } 84 | } 85 | } 86 | 87 | // WriteString writes string. which is implemented io.StringWriter. 88 | func (m *MoveWriter) WriteString(s string) (nn int, _ error) { 89 | for _, char := range s { 90 | if char == '\n' { 91 | n, _ := fmt.Fprintf(m.w, "\x1b[%d;%dH%s\x1b[0K", 92 | m.y+m.idx, 93 | m.x, 94 | m.buf.String(), 95 | ) 96 | m.buf.Reset() 97 | m.idx++ 98 | nn += n 99 | } else { 100 | m.buf.WriteRune(char) 101 | } 102 | } 103 | if m.buf.Len() > 0 { 104 | n, _ := fmt.Fprintf(m.w, "\x1b[%d;%dH%s\x1b[0K", 105 | m.y+m.idx, 106 | m.x, 107 | m.buf.String(), 108 | ) 109 | nn += n 110 | } 111 | return 112 | } 113 | 114 | // getXY gets relative or absolute coorditantes 115 | // To get relative, set PCT flag to number: 116 | // 117 | // // Get 10% of total width to `x` and 20 to y 118 | // x, y = tm.GetXY(10|tm.PCT, 20) 119 | // 120 | func getXY(x int, y int) (int, int) { 121 | // Set percent flag: num | PCT 122 | // 123 | // Check percent flag: num & PCT 124 | // 125 | // Reset percent flag: num & 0xFF 126 | const shift = uint(^uint(0)>>63) << 4 127 | const PCT = 0x8000 << shift 128 | if y == -1 { 129 | y = currentHeight() + 1 130 | } 131 | 132 | if x&PCT != 0 { 133 | x = int((x & 0xFF) * Width() / 100) 134 | } 135 | 136 | if y&PCT != 0 { 137 | y = int((y & 0xFF) * Height() / 100) 138 | } 139 | 140 | return x, y 141 | } 142 | 143 | // currentHeight returns current height. Line count in Screen buffer. 144 | func currentHeight() int { 145 | return strings.Count(buffer.String(), "\n") 146 | } 147 | -------------------------------------------------------------------------------- /cmd/internal/screen/screen.go: -------------------------------------------------------------------------------- 1 | package screen 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | 7 | colorable "github.com/mattn/go-colorable" 8 | "golang.org/x/crypto/ssh/terminal" 9 | ) 10 | 11 | // Stdout color supported stdout 12 | var Stdout = colorable.NewColorableStdout() 13 | 14 | // SaveState saves cursor state. 15 | func SaveState() { Stdout.Write([]byte("\0337")) } 16 | 17 | // RestoreState restores cursor state. 18 | func RestoreState() { Stdout.Write([]byte("\0338")) } 19 | 20 | // Clear clears terminal screen. 21 | func Clear() { Stdout.Write([]byte("\033[2J")) } 22 | 23 | // HideCursor hide the cursor 24 | func HideCursor() { Stdout.Write([]byte("\033[?25l")) } 25 | 26 | // UnHideCursor unhide the cursor 27 | func UnHideCursor() { Stdout.Write([]byte("\033[?25h")) } 28 | 29 | var size struct { 30 | once sync.Once 31 | width int 32 | height int 33 | } 34 | 35 | func getSize() (int, int) { 36 | size.once.Do(func() { 37 | var err error 38 | size.width, size.height, err = terminal.GetSize(int(os.Stdout.Fd())) 39 | if err != nil { 40 | size.width, size.height = -1, -1 41 | } 42 | }) 43 | return size.width, size.height 44 | } 45 | 46 | // Width returns console width 47 | func Width() int { 48 | width, _ := getSize() 49 | return width 50 | } 51 | 52 | // Height returns console height 53 | func Height() int { 54 | _, height := getSize() 55 | return height 56 | } 57 | -------------------------------------------------------------------------------- /cmd/internal/super/supercow.go: -------------------------------------------------------------------------------- 1 | package super 2 | 3 | import ( 4 | "errors" 5 | "io" 6 | "os" 7 | "os/signal" 8 | "strings" 9 | "syscall" 10 | "time" 11 | 12 | "github.com/Code-Hex/Neo-cowsay/cmd/v2/internal/screen" 13 | cowsay "github.com/Code-Hex/Neo-cowsay/v2" 14 | "github.com/Code-Hex/Neo-cowsay/v2/decoration" 15 | runewidth "github.com/mattn/go-runewidth" 16 | "github.com/rivo/uniseg" 17 | ) 18 | 19 | func getNoSaidCow(cow *cowsay.Cow, opts ...cowsay.Option) (string, error) { 20 | opts = append(opts, cowsay.Thoughts(' ')) 21 | cow, err := cow.Clone(opts...) 22 | if err != nil { 23 | return "", err 24 | } 25 | return cow.GetCow() 26 | } 27 | 28 | // RunSuperCow runs super cow mode animation on the your terminal 29 | func RunSuperCow(phrase string, withBold bool, opts ...cowsay.Option) error { 30 | cow, err := cowsay.New(opts...) 31 | if err != nil { 32 | return err 33 | } 34 | balloon := cow.Balloon(phrase) 35 | blank := createBlankSpace(balloon) 36 | 37 | said, err := cow.GetCow() 38 | if err != nil { 39 | return err 40 | } 41 | 42 | notSaid, err := getNoSaidCow(cow, opts...) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | saidCow := balloon + said 48 | saidCowLines := strings.Count(saidCow, "\n") + 1 49 | 50 | // When it is higher than the height of the terminal 51 | h := screen.Height() 52 | if saidCowLines > h { 53 | return errors.New("too height messages") 54 | } 55 | 56 | notSaidCow := blank + notSaid 57 | 58 | renderer := newRenderer(saidCow, notSaidCow) 59 | 60 | screen.SaveState() 61 | screen.HideCursor() 62 | screen.Clear() 63 | 64 | go renderer.createFrames(cow, withBold) 65 | 66 | renderer.render() 67 | 68 | screen.UnHideCursor() 69 | screen.RestoreState() 70 | 71 | return nil 72 | } 73 | 74 | func createBlankSpace(balloon string) string { 75 | var buf strings.Builder 76 | l := strings.Count(balloon, "\n") 77 | for i := 0; i < l; i++ { 78 | buf.WriteRune('\n') 79 | } 80 | return buf.String() 81 | } 82 | 83 | func maxLen(cow []string) int { 84 | max := 0 85 | for _, line := range cow { 86 | l := runewidth.StringWidth(line) 87 | if max < l { 88 | max = l 89 | } 90 | } 91 | return max 92 | } 93 | 94 | type cowLine struct { 95 | raw string 96 | clusters []rune 97 | } 98 | 99 | func (c *cowLine) Len() int { 100 | return len(c.clusters) 101 | } 102 | 103 | func (c *cowLine) Slice(i, j int) string { 104 | if c.Len() == 0 { 105 | return "" 106 | } 107 | return string(c.clusters[i:j]) 108 | } 109 | 110 | func makeCowLines(cow string) []*cowLine { 111 | sep := strings.Split(cow, "\n") 112 | cowLines := make([]*cowLine, len(sep)) 113 | for i, line := range sep { 114 | g := uniseg.NewGraphemes(line) 115 | clusters := make([]rune, 0) 116 | for g.Next() { 117 | clusters = append(clusters, g.Runes()...) 118 | } 119 | cowLines[i] = &cowLine{ 120 | raw: line, 121 | clusters: clusters, 122 | } 123 | } 124 | return cowLines 125 | } 126 | 127 | type renderer struct { 128 | max int 129 | middle int 130 | screenWidth int 131 | heightDiff int 132 | frames chan string 133 | 134 | saidCow string 135 | notSaidCowLines []*cowLine 136 | 137 | quit chan os.Signal 138 | } 139 | 140 | func newRenderer(saidCow, notSaidCow string) *renderer { 141 | notSaidCowSep := strings.Split(notSaidCow, "\n") 142 | w, cowsWidth := screen.Width(), maxLen(notSaidCowSep) 143 | max := w + cowsWidth 144 | 145 | quit := make(chan os.Signal, 1) 146 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) 147 | 148 | return &renderer{ 149 | max: max, 150 | middle: max / 2, 151 | screenWidth: w, 152 | heightDiff: screen.Height() - strings.Count(saidCow, "\n") - 1, 153 | frames: make(chan string, max), 154 | saidCow: saidCow, 155 | notSaidCowLines: makeCowLines(notSaidCow), 156 | quit: quit, 157 | } 158 | } 159 | 160 | const ( 161 | // Frequency the color changes 162 | magic = 2 163 | 164 | span = 30 * time.Millisecond 165 | standup = 3 * time.Second 166 | ) 167 | 168 | func (r *renderer) createFrames(cow *cowsay.Cow, withBold bool) { 169 | const times = standup / span 170 | w := r.newWriter(withBold) 171 | 172 | for x, i := 0, 1; i <= r.max; i++ { 173 | if i == r.middle { 174 | w.SetPosx(r.posX(i)) 175 | for k := 0; k < int(times); k++ { 176 | base := x * 70 177 | // draw colored cow 178 | w.SetColorSeq(base) 179 | w.WriteString(r.saidCow) 180 | r.frames <- w.String() 181 | if k%magic == 0 { 182 | x++ 183 | } 184 | } 185 | } else { 186 | base := x * 70 187 | w.SetPosx(r.posX(i)) 188 | w.SetColorSeq(base) 189 | 190 | for _, line := range r.notSaidCowLines { 191 | if i > r.screenWidth { 192 | // Left side animations 193 | n := i - r.screenWidth 194 | if n < line.Len() { 195 | w.WriteString(line.Slice(n, line.Len())) 196 | } 197 | } else if i <= line.Len() { 198 | // Right side animations 199 | w.WriteString(line.Slice(0, i-1)) 200 | } else { 201 | w.WriteString(line.raw) 202 | } 203 | w.Write([]byte{'\n'}) 204 | } 205 | r.frames <- w.String() 206 | } 207 | if i%magic == 0 { 208 | x++ 209 | } 210 | } 211 | close(r.frames) 212 | } 213 | 214 | func (r *renderer) render() { 215 | initCh := make(chan struct{}, 1) 216 | initCh <- struct{}{} 217 | 218 | for view := range r.frames { 219 | select { 220 | case <-r.quit: 221 | screen.Clear() 222 | return 223 | case <-initCh: 224 | case <-time.After(span): 225 | } 226 | io.Copy(screen.Stdout, strings.NewReader(view)) 227 | } 228 | } 229 | 230 | func (r *renderer) posX(i int) int { 231 | posx := r.screenWidth - i 232 | if posx < 1 { 233 | posx = 1 234 | } 235 | return posx 236 | } 237 | 238 | // Writer is wrapper which is both screen.MoveWriter and decoration.Writer. 239 | type Writer struct { 240 | buf *strings.Builder 241 | mw *screen.MoveWriter 242 | dw *decoration.Writer 243 | } 244 | 245 | func (r *renderer) newWriter(withBold bool) *Writer { 246 | var buf strings.Builder 247 | mw := screen.NewMoveWriter(&buf, r.posX(0), r.heightDiff) 248 | options := []decoration.Option{ 249 | decoration.WithAurora(0), 250 | } 251 | if withBold { 252 | options = append(options, decoration.WithBold()) 253 | } 254 | dw := decoration.NewWriter(mw, options...) 255 | return &Writer{ 256 | buf: &buf, 257 | mw: mw, 258 | dw: dw, 259 | } 260 | } 261 | 262 | // WriteString writes string. which is implemented io.StringWriter. 263 | func (w *Writer) WriteString(s string) (int, error) { return w.dw.WriteString(s) } 264 | 265 | // Write writes bytes. which is implemented io.Writer. 266 | func (w *Writer) Write(p []byte) (int, error) { return w.dw.Write(p) } 267 | 268 | // SetPosx sets posx 269 | func (w *Writer) SetPosx(x int) { w.mw.SetPosx(x) } 270 | 271 | // SetColorSeq sets color sequence. 272 | func (w *Writer) SetColorSeq(colorSeq int) { w.dw.SetColorSeq(colorSeq) } 273 | 274 | // Reset resets calls some Reset methods. 275 | func (w *Writer) Reset() { 276 | w.buf.Reset() 277 | w.mw.Reset() 278 | } 279 | 280 | func (w *Writer) String() string { 281 | defer w.Reset() 282 | return w.buf.String() 283 | } 284 | -------------------------------------------------------------------------------- /cmd/testdata/cowsay/W_option.txt: -------------------------------------------------------------------------------- 1 | _____ 2 | / foo \ 3 | | bar | 4 | \ baz / 5 | ----- 6 | \ ^__^ 7 | \ (oo)\_______ 8 | (__)\ )\/\ 9 | ||----w | 10 | || || 11 | -------------------------------------------------------------------------------- /cmd/testdata/cowsay/b_option.txt: -------------------------------------------------------------------------------- 1 | ___________ 2 | < foobarbaz > 3 | ----------- 4 | \ ^__^ 5 | \ (==)\_______ 6 | (__)\ )\/\ 7 | ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowsay/d_option.txt: -------------------------------------------------------------------------------- 1 | ____________ 2 | < 0xdeadbeef > 3 | ------------ 4 | \ ^__^ 5 | \ (xx)\_______ 6 | (__)\ )\/\ 7 | U ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowsay/eyes_option.txt: -------------------------------------------------------------------------------- 1 | _______________ 2 | < I'm not angry > 3 | --------------- 4 | \ ^__^ 5 | \ (^^)\_______ 6 | (__)\ )\/\ 7 | ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowsay/f_tux_option.txt: -------------------------------------------------------------------------------- 1 | ________________ 2 | < what is macOS? > 3 | ---------------- 4 | \ 5 | \ 6 | .--. 7 | |o_o | 8 | |:_/ | 9 | // \ \ 10 | (| | ) 11 | /'\_ _/`\ 12 | \___)=(___/ 13 | 14 | -------------------------------------------------------------------------------- /cmd/testdata/cowsay/g_option.txt: -------------------------------------------------------------------------------- 1 | _______________ 2 | < give me money > 3 | --------------- 4 | \ ^__^ 5 | \ ($$)\_______ 6 | (__)\ )\/\ 7 | ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowsay/n_option.txt: -------------------------------------------------------------------------------- 1 | _____ 2 | / foo \ 3 | | bar | 4 | \ baz / 5 | ----- 6 | \ ^__^ 7 | \ (oo)\_______ 8 | (__)\ )\/\ 9 | ||----w | 10 | || || 11 | -------------------------------------------------------------------------------- /cmd/testdata/cowsay/p_option.txt: -------------------------------------------------------------------------------- 1 | ___________________ 2 | < everyone hates me > 3 | ------------------- 4 | \ ^__^ 5 | \ (@@)\_______ 6 | (__)\ )\/\ 7 | ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowsay/s_option.txt: -------------------------------------------------------------------------------- 1 | ______________ 2 | < I don't know > 3 | -------------- 4 | \ ^__^ 5 | \ (**)\_______ 6 | (__)\ )\/\ 7 | U ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowsay/t_option.txt: -------------------------------------------------------------------------------- 1 | _______ 2 | < tired > 3 | ------- 4 | \ ^__^ 5 | \ (--)\_______ 6 | (__)\ )\/\ 7 | ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowsay/tongue_option.txt: -------------------------------------------------------------------------------- 1 | ________ 2 | < hungry > 3 | -------- 4 | \ ^__^ 5 | \ (oo)\_______ 6 | (__)\ )\/\ 7 | : ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowsay/wired_option.txt: -------------------------------------------------------------------------------- 1 | __________________________ 2 | < Wanna Netflix and chill? > 3 | -------------------------- 4 | \ ^__^ 5 | \ (OO)\_______ 6 | (__)\ )\/\ 7 | ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowsay/y_option.txt: -------------------------------------------------------------------------------- 1 | ________________________ 2 | < I forgot my ID at home > 3 | ------------------------ 4 | \ ^__^ 5 | \ (..)\_______ 6 | (__)\ )\/\ 7 | ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowthink/W_option.txt: -------------------------------------------------------------------------------- 1 | _____ 2 | ( foo ) 3 | ( bar ) 4 | ( baz ) 5 | ----- 6 | o ^__^ 7 | o (oo)\_______ 8 | (__)\ )\/\ 9 | ||----w | 10 | || || 11 | -------------------------------------------------------------------------------- /cmd/testdata/cowthink/b_option.txt: -------------------------------------------------------------------------------- 1 | ___________ 2 | ( foobarbaz ) 3 | ----------- 4 | o ^__^ 5 | o (==)\_______ 6 | (__)\ )\/\ 7 | ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowthink/d_option.txt: -------------------------------------------------------------------------------- 1 | ____________ 2 | ( 0xdeadbeef ) 3 | ------------ 4 | o ^__^ 5 | o (xx)\_______ 6 | (__)\ )\/\ 7 | U ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowthink/eyes_option.txt: -------------------------------------------------------------------------------- 1 | _______________ 2 | ( I'm not angry ) 3 | --------------- 4 | o ^__^ 5 | o (^^)\_______ 6 | (__)\ )\/\ 7 | ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowthink/f_tux_option.txt: -------------------------------------------------------------------------------- 1 | ________________ 2 | ( what is macOS? ) 3 | ---------------- 4 | o 5 | o 6 | .--. 7 | |o_o | 8 | |:_/ | 9 | // \ \ 10 | (| | ) 11 | /'\_ _/`\ 12 | \___)=(___/ 13 | 14 | -------------------------------------------------------------------------------- /cmd/testdata/cowthink/g_option.txt: -------------------------------------------------------------------------------- 1 | _______________ 2 | ( give me money ) 3 | --------------- 4 | o ^__^ 5 | o ($$)\_______ 6 | (__)\ )\/\ 7 | ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowthink/n_option.txt: -------------------------------------------------------------------------------- 1 | _____ 2 | ( foo ) 3 | ( bar ) 4 | ( baz ) 5 | ----- 6 | o ^__^ 7 | o (oo)\_______ 8 | (__)\ )\/\ 9 | ||----w | 10 | || || 11 | -------------------------------------------------------------------------------- /cmd/testdata/cowthink/p_option.txt: -------------------------------------------------------------------------------- 1 | ___________________ 2 | ( everyone hates me ) 3 | ------------------- 4 | o ^__^ 5 | o (@@)\_______ 6 | (__)\ )\/\ 7 | ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowthink/s_option.txt: -------------------------------------------------------------------------------- 1 | ______________ 2 | ( I don't know ) 3 | -------------- 4 | o ^__^ 5 | o (**)\_______ 6 | (__)\ )\/\ 7 | U ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowthink/t_option.txt: -------------------------------------------------------------------------------- 1 | _______ 2 | ( tired ) 3 | ------- 4 | o ^__^ 5 | o (--)\_______ 6 | (__)\ )\/\ 7 | ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowthink/tongue_option.txt: -------------------------------------------------------------------------------- 1 | ________ 2 | ( hungry ) 3 | -------- 4 | o ^__^ 5 | o (oo)\_______ 6 | (__)\ )\/\ 7 | : ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowthink/wired_option.txt: -------------------------------------------------------------------------------- 1 | __________________________ 2 | ( Wanna Netflix and chill? ) 3 | -------------------------- 4 | o ^__^ 5 | o (OO)\_______ 6 | (__)\ )\/\ 7 | ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cmd/testdata/cowthink/y_option.txt: -------------------------------------------------------------------------------- 1 | ________________________ 2 | ( I forgot my ID at home ) 3 | ------------------------ 4 | o ^__^ 5 | o (..)\_______ 6 | (__)\ )\/\ 7 | ||----w | 8 | || || 9 | -------------------------------------------------------------------------------- /cow.go: -------------------------------------------------------------------------------- 1 | package cowsay 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strings" 7 | ) 8 | 9 | // Cow struct!! 10 | type Cow struct { 11 | eyes string 12 | tongue string 13 | typ *CowFile 14 | thoughts rune 15 | thinking bool 16 | ballonWidth int 17 | disableWordWrap bool 18 | 19 | buf strings.Builder 20 | } 21 | 22 | // New returns pointer of Cow struct that made by options 23 | func New(options ...Option) (*Cow, error) { 24 | cow := &Cow{ 25 | eyes: "oo", 26 | tongue: " ", 27 | thoughts: '\\', 28 | typ: &CowFile{ 29 | Name: "default", 30 | BasePath: "cows", 31 | LocationType: InBinary, 32 | }, 33 | ballonWidth: 40, 34 | } 35 | for _, o := range options { 36 | if err := o(cow); err != nil { 37 | return nil, err 38 | } 39 | } 40 | return cow, nil 41 | } 42 | 43 | // Say returns string that said by cow 44 | func (cow *Cow) Say(phrase string) (string, error) { 45 | mow, err := cow.GetCow() 46 | if err != nil { 47 | return "", err 48 | } 49 | return cow.Balloon(phrase) + mow, nil 50 | } 51 | 52 | // Clone returns a copy of cow. 53 | // 54 | // If any options are specified, they will be reflected. 55 | func (cow *Cow) Clone(options ...Option) (*Cow, error) { 56 | ret := new(Cow) 57 | *ret = *cow 58 | ret.buf.Reset() 59 | for _, o := range options { 60 | if err := o(ret); err != nil { 61 | return nil, err 62 | } 63 | } 64 | return ret, nil 65 | } 66 | 67 | // Option defined for Options 68 | type Option func(*Cow) error 69 | 70 | // Eyes specifies eyes 71 | // The specified string will always be adjusted to be equal to two characters. 72 | func Eyes(s string) Option { 73 | return func(c *Cow) error { 74 | c.eyes = adjustTo2Chars(s) 75 | return nil 76 | } 77 | } 78 | 79 | // Tongue specifies tongue 80 | // The specified string will always be adjusted to be less than or equal to two characters. 81 | func Tongue(s string) Option { 82 | return func(c *Cow) error { 83 | c.tongue = adjustTo2Chars(s) 84 | return nil 85 | } 86 | } 87 | 88 | func adjustTo2Chars(s string) string { 89 | if len(s) >= 2 { 90 | return s[:2] 91 | } 92 | if len(s) == 1 { 93 | return s + " " 94 | } 95 | return " " 96 | } 97 | 98 | func containCows(target string) (*CowFile, error) { 99 | cowPaths, err := Cows() 100 | if err != nil { 101 | return nil, err 102 | } 103 | for _, cowPath := range cowPaths { 104 | cowfile, ok := cowPath.Lookup(target) 105 | if ok { 106 | return cowfile, nil 107 | } 108 | } 109 | return nil, nil 110 | } 111 | 112 | // NotFound is indicated not found the cowfile. 113 | type NotFound struct { 114 | Cowfile string 115 | } 116 | 117 | var _ error = (*NotFound)(nil) 118 | 119 | func (n *NotFound) Error() string { 120 | return fmt.Sprintf("not found %q cowfile", n.Cowfile) 121 | } 122 | 123 | // Type specify name of the cowfile 124 | func Type(s string) Option { 125 | if s == "" { 126 | s = "default" 127 | } 128 | return func(c *Cow) error { 129 | cowfile, err := containCows(s) 130 | if err != nil { 131 | return err 132 | } 133 | if cowfile != nil { 134 | c.typ = cowfile 135 | return nil 136 | } 137 | return &NotFound{Cowfile: s} 138 | } 139 | } 140 | 141 | // Thinking enables thinking mode 142 | func Thinking() Option { 143 | return func(c *Cow) error { 144 | c.thinking = true 145 | return nil 146 | } 147 | } 148 | 149 | // Thoughts Thoughts allows you to specify 150 | // the rune that will be drawn between 151 | // the speech bubbles and the cow 152 | func Thoughts(thoughts rune) Option { 153 | return func(c *Cow) error { 154 | c.thoughts = thoughts 155 | return nil 156 | } 157 | } 158 | 159 | // Random specifies something .cow from cows directory 160 | func Random() Option { 161 | pick, err := pickCow() 162 | return func(c *Cow) error { 163 | if err != nil { 164 | return err 165 | } 166 | c.typ = pick 167 | return nil 168 | } 169 | } 170 | 171 | func pickCow() (*CowFile, error) { 172 | cowPaths, err := Cows() 173 | if err != nil { 174 | return nil, err 175 | } 176 | cowPath := cowPaths[rand.Intn(len(cowPaths))] 177 | 178 | n := len(cowPath.CowFiles) 179 | cowfile := cowPath.CowFiles[rand.Intn(n)] 180 | return &CowFile{ 181 | Name: cowfile, 182 | BasePath: cowPath.Name, 183 | LocationType: cowPath.LocationType, 184 | }, nil 185 | } 186 | 187 | // BallonWidth specifies ballon size 188 | func BallonWidth(size uint) Option { 189 | return func(c *Cow) error { 190 | c.ballonWidth = int(size) 191 | return nil 192 | } 193 | } 194 | 195 | // DisableWordWrap disables word wrap. 196 | // Ignoring width of the ballon. 197 | func DisableWordWrap() Option { 198 | return func(c *Cow) error { 199 | c.disableWordWrap = true 200 | return nil 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /cow_test.go: -------------------------------------------------------------------------------- 1 | package cowsay 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/google/go-cmp/cmp" 9 | "github.com/google/go-cmp/cmp/cmpopts" 10 | ) 11 | 12 | func TestCow_Clone(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | opts []Option 16 | from *Cow 17 | want *Cow 18 | }{ 19 | { 20 | name: "without options", 21 | opts: []Option{}, 22 | from: func() *Cow { 23 | cow, _ := New() 24 | return cow 25 | }(), 26 | want: func() *Cow { 27 | cow, _ := New() 28 | return cow 29 | }(), 30 | }, 31 | { 32 | name: "with some options", 33 | opts: []Option{}, 34 | from: func() *Cow { 35 | cow, _ := New( 36 | Type("docker"), 37 | BallonWidth(60), 38 | ) 39 | return cow 40 | }(), 41 | want: func() *Cow { 42 | cow, _ := New( 43 | Type("docker"), 44 | BallonWidth(60), 45 | ) 46 | return cow 47 | }(), 48 | }, 49 | { 50 | name: "clone and some options", 51 | opts: []Option{ 52 | Thinking(), 53 | Thoughts('o'), 54 | }, 55 | from: func() *Cow { 56 | cow, _ := New( 57 | Type("docker"), 58 | BallonWidth(60), 59 | ) 60 | return cow 61 | }(), 62 | want: func() *Cow { 63 | cow, _ := New( 64 | Type("docker"), 65 | BallonWidth(60), 66 | Thinking(), 67 | Thoughts('o'), 68 | ) 69 | return cow 70 | }(), 71 | }, 72 | } 73 | for _, tt := range tests { 74 | t.Run(tt.name, func(t *testing.T) { 75 | got, err := tt.want.Clone(tt.opts...) 76 | if err != nil { 77 | t.Fatal(err) 78 | } 79 | if diff := cmp.Diff(tt.want, got, 80 | cmp.AllowUnexported(Cow{}), 81 | cmpopts.IgnoreFields(Cow{}, "buf")); diff != "" { 82 | t.Errorf("(-want, +got)\n%s", diff) 83 | } 84 | }) 85 | } 86 | 87 | t.Run("random", func(t *testing.T) { 88 | cow, _ := New( 89 | Type(""), 90 | Thinking(), 91 | Thoughts('o'), 92 | Eyes("xx"), 93 | Tongue("u"), 94 | Random(), 95 | ) 96 | 97 | cloned, _ := cow.Clone() 98 | 99 | if diff := cmp.Diff(cow, cloned, 100 | cmp.AllowUnexported(Cow{}), 101 | cmpopts.IgnoreFields(Cow{}, "buf")); diff != "" { 102 | t.Errorf("(-want, +got)\n%s", diff) 103 | } 104 | }) 105 | 106 | t.Run("error", func(t *testing.T) { 107 | cow, err := New() 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | 112 | wantErr := errors.New("error") 113 | _, err = cow.Clone(func(*Cow) error { 114 | return wantErr 115 | }) 116 | if wantErr != err { 117 | t.Fatalf("want %v, but got %v", wantErr, err) 118 | } 119 | }) 120 | } 121 | 122 | func Test_adjustTo2Chars(t *testing.T) { 123 | tests := []struct { 124 | name string 125 | s string 126 | want string 127 | }{ 128 | { 129 | name: "empty", 130 | s: "", 131 | want: " ", 132 | }, 133 | { 134 | name: "1 character", 135 | s: "1", 136 | want: "1 ", 137 | }, 138 | { 139 | name: "2 characters", 140 | s: "12", 141 | want: "12", 142 | }, 143 | { 144 | name: "3 characters", 145 | s: "123", 146 | want: "12", 147 | }, 148 | } 149 | for _, tt := range tests { 150 | t.Run(tt.s, func(t *testing.T) { 151 | if got := adjustTo2Chars(tt.s); got != tt.want { 152 | t.Errorf("adjustTo2Chars() = %v, want %v", got, tt.want) 153 | } 154 | }) 155 | } 156 | } 157 | 158 | func TestNotFound_Error(t *testing.T) { 159 | file := "test" 160 | n := &NotFound{ 161 | Cowfile: file, 162 | } 163 | want := fmt.Sprintf("not found %q cowfile", file) 164 | if want != n.Error() { 165 | t.Fatalf("want %q but got %q", want, n.Error()) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /cows/beavis.zen.cow: -------------------------------------------------------------------------------- 1 | ## 2 | ## Beavis, with Zen philosophy removed. 3 | ## 4 | $the_cow = <> 5.4 3 | ## 4 | $the_cow = < \\ _ -~ `. ^-` ^-_ 19 | ///-._ _ _ _ _ _ _}^ - - - - ~ ~-- ,.-~ 20 | /.-~ 21 | EOC 22 | -------------------------------------------------------------------------------- /cows/elephant-in-snake.cow: -------------------------------------------------------------------------------- 1 | ## 2 | ## Do we need to explain this? 3 | ## 4 | $the_cow = < 16 | ---___ XXX__/ XXXXXX \\__ / 17 | \\- --__/ ___/\\ XXXXXX / ___--/= 18 | \\-\\ ___/ XXXXXX '--- XXXXXX 19 | \\-\\/XXX\\ XXXXXX /XXXXX 20 | \\XXXXXXXXX \\ /XXXXX/ 21 | \\XXXXXX > _/XXXXX/ 22 | \\XXXXX--__/ __-- XXXX/ 23 | -XXXXXXXX--------------- XXXXXX- 24 | \\XXXXXXXXXXXXXXXXXXXXXXXXXX/ 25 | ""VXXXXXXXXXXXXXXXXXXV"" 26 | EOC 27 | -------------------------------------------------------------------------------- /cows/gopher.cow: -------------------------------------------------------------------------------- 1 | $the_cow = < 21 | EOC 22 | -------------------------------------------------------------------------------- /cows/moofasa.cow: -------------------------------------------------------------------------------- 1 | ## 2 | ## MOOfasa. 3 | ## 4 | $the_cow = <> 12 | EOC 13 | -------------------------------------------------------------------------------- /cows/squirrel.cow: -------------------------------------------------------------------------------- 1 | $the_cow = < < > .---. 8 | $thoughts | \\ \\ - ~ ~ - / / | 9 | _____ ..-~ ~-..-~ 10 | | | \\~~~\\.' `./~~~/ 11 | --------- \\__/ \\__/ 12 | .' O \\ / / \\ " 13 | (_____, `._.' | } \\/~~~/ 14 | `----. / } | / \\__/ 15 | `-. | / | / `. ,~~| 16 | ~-.__| /_ - ~ ^| /- _ `..-' 17 | | / | / ~-. `-. _ _ _ 18 | |_____| |_____| ~ - . _ _ _ _ _> 19 | EOC 20 | -------------------------------------------------------------------------------- /cows/stimpy.cow: -------------------------------------------------------------------------------- 1 | ## 2 | ## Stimpy! 3 | ## 4 | $the_cow = <> 12 | EOC 13 | -------------------------------------------------------------------------------- /cows/turkey.cow: -------------------------------------------------------------------------------- 1 | ## 2 | ## Turkey! 3 | ## 4 | $the_cow = <____) >___ ^\\_\\_\\_\\_\\_\\_\\) 24 | ^^^//\\\\_^^//\\\\_^ ^(\\_\\_\\_\\) 25 | ^^^ ^^ ^^^ ^ 26 | EOC 27 | -------------------------------------------------------------------------------- /cows/turtle.cow: -------------------------------------------------------------------------------- 1 | ## 2 | ## A mysterious turtle... 3 | ## 4 | $the_cow = < 161 | -------- 162 | \ ^__^ 163 | \ (oo)\_______ 164 | (__)\ )\/\ 165 | ||----w | 166 | || ||` 167 | 168 | func TestSay(t *testing.T) { 169 | type args struct { 170 | phrase string 171 | options []Option 172 | } 173 | tests := []struct { 174 | name string 175 | args args 176 | wantFile string 177 | wantErr bool 178 | }{ 179 | { 180 | name: "default", 181 | args: args{ 182 | phrase: "hello!", 183 | }, 184 | wantFile: "default.cow", 185 | wantErr: false, 186 | }, 187 | { 188 | name: "nest", 189 | args: args{ 190 | phrase: defaultSay, 191 | options: []Option{ 192 | DisableWordWrap(), 193 | }, 194 | }, 195 | wantFile: "nest.cow", 196 | wantErr: false, 197 | }, 198 | { 199 | name: "error", 200 | args: args{ 201 | phrase: "error", 202 | options: []Option{ 203 | func(*Cow) error { 204 | return errors.New("error") 205 | }, 206 | }, 207 | }, 208 | wantErr: true, 209 | }, 210 | } 211 | for _, tt := range tests { 212 | t.Run(tt.name, func(t *testing.T) { 213 | got, err := Say(tt.args.phrase, tt.args.options...) 214 | if (err != nil) != tt.wantErr { 215 | t.Errorf("Say() error = %v, wantErr %v", err, tt.wantErr) 216 | return 217 | } 218 | if tt.wantErr { 219 | return 220 | } 221 | filename := filepath.Join("testdata", tt.wantFile) 222 | content, err := ioutil.ReadFile(filename) 223 | if err != nil { 224 | t.Fatal(err) 225 | } 226 | got = strings.Replace(got, "\r", "", -1) // for windows 227 | want := strings.Replace(string(content), "\r", "", -1) // for windows 228 | if want != got { 229 | t.Log(cmp.Diff([]byte(want), []byte(got))) 230 | t.Fatalf("want\n%s\n\ngot\n%s", want, got) 231 | } 232 | }) 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /decoration/aurora.go: -------------------------------------------------------------------------------- 1 | package decoration 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "math" 7 | "unicode" 8 | "unicode/utf8" 9 | ) 10 | 11 | func (w *Writer) writeAsAurora(b []byte) (nn int, err error) { 12 | defer w.buf.Reset() 13 | 14 | for len(b) > 0 { 15 | char, size := utf8.DecodeRune(b) 16 | if char == '\n' { 17 | w.buf.WriteRune(char) 18 | b = b[size:] 19 | continue 20 | } 21 | if unicode.IsSpace(char) { 22 | w.buf.WriteRune(char) 23 | } else { 24 | fmt.Fprintf(&w.buf, "\033[38;5;%d%sm%c\033[0m", 25 | rgb(float64(w.options.colorSeq)), 26 | w.options.maybeBold(), 27 | char, 28 | ) 29 | } 30 | w.options.colorSeq++ 31 | b = b[size:] 32 | } 33 | 34 | return w.writer.Write(w.buf.Bytes()) 35 | } 36 | 37 | func (w *Writer) writeStringAsAurora(s string) (nn int, err error) { 38 | defer w.buf.Reset() 39 | 40 | for _, char := range s { 41 | if char == '\n' { 42 | w.buf.WriteRune(char) 43 | continue 44 | } 45 | if unicode.IsSpace(char) { 46 | w.buf.WriteRune(char) 47 | } else { 48 | fmt.Fprintf(&w.buf, "\033[38;5;%d%sm%c\033[0m", 49 | rgb(float64(w.options.colorSeq)), 50 | w.options.maybeBold(), 51 | char, 52 | ) 53 | } 54 | w.options.colorSeq++ 55 | } 56 | if sw, ok := w.writer.(io.StringWriter); ok { 57 | return sw.WriteString(w.buf.String()) 58 | } 59 | return w.writer.Write(w.buf.Bytes()) 60 | } 61 | 62 | // https://sking7.github.io/articles/139888127.html#:~:text=value%20of%20frequency.-,Using,-out-of-phase 63 | const ( 64 | freq = 0.01 65 | m = math.Pi / 3 66 | 67 | redPhase = 0 68 | greenPhase = 2 * m 69 | bluePhase = 4 * m 70 | ) 71 | 72 | var rgbMemo = map[float64]int64{} 73 | 74 | func rgb(i float64) int64 { 75 | if v, ok := rgbMemo[i]; ok { 76 | return v 77 | } 78 | red := int64(6*(math.Sin(freq*i+redPhase)*127+128)/256) * 36 79 | green := int64(6*(math.Sin(freq*i+greenPhase)*127+128)/256) * 6 80 | blue := int64(6*(math.Sin(freq*i+bluePhase)*127+128)/256) * 1 81 | rgbMemo[i] = 16 + red + green + blue 82 | return rgbMemo[i] 83 | } 84 | -------------------------------------------------------------------------------- /decoration/bold.go: -------------------------------------------------------------------------------- 1 | package decoration 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "unicode" 7 | "unicode/utf8" 8 | ) 9 | 10 | func (w *Writer) writeAsDefaultBold(b []byte) (nn int, err error) { 11 | defer w.buf.Reset() 12 | 13 | for len(b) > 0 { 14 | char, size := utf8.DecodeRune(b) 15 | if char == '\n' { 16 | w.buf.WriteRune(char) 17 | b = b[size:] 18 | continue 19 | } 20 | if unicode.IsSpace(char) { 21 | w.buf.WriteRune(char) 22 | } else { 23 | fmt.Fprintf(&w.buf, "\x1b[1m%c\x1b[0m", char) 24 | } 25 | b = b[size:] 26 | } 27 | return w.writer.Write(w.buf.Bytes()) 28 | } 29 | 30 | func (w *Writer) writeStringAsDefaultBold(s string) (nn int, err error) { 31 | defer w.buf.Reset() 32 | 33 | for _, char := range s { 34 | if char == '\n' { 35 | w.buf.WriteRune(char) 36 | continue 37 | } 38 | if unicode.IsSpace(char) { 39 | w.buf.WriteRune(char) 40 | } else { 41 | fmt.Fprintf(&w.buf, "\x1b[1m%c\x1b[0m", char) 42 | } 43 | } 44 | if sw, ok := w.writer.(io.StringWriter); ok { 45 | return sw.WriteString(w.buf.String()) 46 | } 47 | return w.writer.Write(w.buf.Bytes()) 48 | } 49 | -------------------------------------------------------------------------------- /decoration/decoration.go: -------------------------------------------------------------------------------- 1 | package decoration 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | ) 7 | 8 | type options struct { 9 | withBold bool 10 | withRainbow bool 11 | withAurora bool 12 | colorSeq int 13 | } 14 | 15 | func (o *options) maybeBold() string { 16 | if o.withBold { 17 | return ";1" 18 | } 19 | return "" 20 | } 21 | 22 | // Option for any writer in this package. 23 | type Option func(o *options) 24 | 25 | // WithBold writes with bold. 26 | func WithBold() Option { 27 | return func(o *options) { 28 | o.withBold = true 29 | } 30 | } 31 | 32 | // WithRainbow writes with rainbow. 33 | func WithRainbow() Option { 34 | return func(o *options) { 35 | o.withRainbow = true 36 | } 37 | } 38 | 39 | // WithAurora writes with aurora. 40 | func WithAurora(initialSeq int) Option { 41 | return func(o *options) { 42 | o.withAurora = true 43 | o.colorSeq = initialSeq 44 | } 45 | } 46 | 47 | // Writer is a writer to decorates. 48 | type Writer struct { 49 | writer io.Writer 50 | buf bytes.Buffer 51 | options *options 52 | } 53 | 54 | var _ interface { 55 | io.Writer 56 | io.StringWriter 57 | } = (*Writer)(nil) 58 | 59 | // NewWriter creates a new writer. 60 | func NewWriter(w io.Writer, opts ...Option) *Writer { 61 | options := new(options) 62 | for _, optFunc := range opts { 63 | optFunc(options) 64 | } 65 | return &Writer{ 66 | writer: w, 67 | options: options, 68 | } 69 | } 70 | 71 | // SetColorSeq sets current color sequence. 72 | func (w *Writer) SetColorSeq(colorSeq int) { 73 | w.options.colorSeq = colorSeq 74 | } 75 | 76 | // Write writes bytes. which is implemented io.Writer. 77 | // 78 | // If Bold is enabled in the options, the text will be written as Bold. 79 | // If both Aurora and Rainbow are enabled in the options, Aurora will take precedence. 80 | func (w *Writer) Write(b []byte) (nn int, err error) { 81 | switch { 82 | case w.options.withAurora: 83 | return w.writeAsAurora(b) 84 | case w.options.withRainbow: 85 | return w.writeAsRainbow(b) 86 | case w.options.withBold: 87 | return w.writeAsDefaultBold(b) 88 | default: 89 | return w.writer.Write(b) 90 | } 91 | } 92 | 93 | // WriteString writes string. which is implemented io.StringWriter. 94 | // 95 | // See also Write. 96 | func (w *Writer) WriteString(s string) (n int, err error) { 97 | switch { 98 | case w.options.withAurora: 99 | return w.writeStringAsAurora(s) 100 | case w.options.withRainbow: 101 | return w.writeStringAsRainbow(s) 102 | case w.options.withBold: 103 | return w.writeStringAsDefaultBold(s) 104 | default: 105 | if sw, ok := w.writer.(io.StringWriter); ok { 106 | return sw.WriteString(w.buf.String()) 107 | } 108 | return w.writer.Write([]byte(s)) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /decoration/rainbow.go: -------------------------------------------------------------------------------- 1 | package decoration 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "unicode" 7 | "unicode/utf8" 8 | ) 9 | 10 | const ( 11 | red = iota + 31 12 | green 13 | yellow 14 | blue 15 | magenta 16 | cyan 17 | ) 18 | 19 | var rainbow = []int{magenta, red, yellow, green, cyan, blue} 20 | 21 | func (w *Writer) writeAsRainbow(b []byte) (nn int, err error) { 22 | defer w.buf.Reset() 23 | 24 | for len(b) > 0 { 25 | char, size := utf8.DecodeRune(b) 26 | if char == '\n' { 27 | w.options.colorSeq = 0 28 | w.buf.WriteRune(char) 29 | b = b[size:] 30 | continue 31 | } 32 | if unicode.IsSpace(char) { 33 | w.buf.WriteRune(char) 34 | } else { 35 | fmt.Fprintf(&w.buf, "\x1b[%d%sm%c\x1b[0m", 36 | rainbow[w.options.colorSeq%len(rainbow)], 37 | w.options.maybeBold(), 38 | char, 39 | ) 40 | } 41 | w.options.colorSeq++ 42 | b = b[size:] 43 | } 44 | 45 | return w.writer.Write(w.buf.Bytes()) 46 | } 47 | 48 | func (w *Writer) writeStringAsRainbow(s string) (nn int, err error) { 49 | defer w.buf.Reset() 50 | 51 | for _, char := range s { 52 | if char == '\n' { 53 | w.options.colorSeq = 0 54 | w.buf.WriteRune(char) 55 | continue 56 | } 57 | if unicode.IsSpace(char) { 58 | w.buf.WriteRune(char) 59 | } else { 60 | fmt.Fprintf(&w.buf, "\x1b[%d%sm%c\x1b[0m", 61 | rainbow[w.options.colorSeq%len(rainbow)], 62 | w.options.maybeBold(), 63 | char, 64 | ) 65 | } 66 | w.options.colorSeq++ 67 | } 68 | 69 | if sw, ok := w.writer.(io.StringWriter); ok { 70 | return sw.WriteString(w.buf.String()) 71 | } 72 | return w.writer.Write(w.buf.Bytes()) 73 | } 74 | -------------------------------------------------------------------------------- /doc/cowsay.1: -------------------------------------------------------------------------------- 1 | '\" t 2 | .\" Title: cowsay 3 | .\" Author: [see the "AUTHOR(S)" section] 4 | .\" Generator: Asciidoctor 2.0.16 5 | .\" Date: 2021-11-13 6 | .\" Manual: \ \& 7 | .\" Source: \ \& 8 | .\" Language: English 9 | .\" 10 | .TH "NEO COWSAY/COWTHINK" "1" "2021-11-13" "\ \&" "\ \&" 11 | .ie \n(.g .ds Aq \(aq 12 | .el .ds Aq ' 13 | .ss \n[.ss] 0 14 | .nh 15 | .ad l 16 | .de URL 17 | \fI\\$2\fP <\\$1>\\$3 18 | .. 19 | .als MTO URL 20 | .if \n[.g] \{\ 21 | . mso www.tmac 22 | . am URL 23 | . ad l 24 | . . 25 | . am MTO 26 | . ad l 27 | . . 28 | . LINKSTYLE blue R < > 29 | .\} 30 | .SH "NAME" 31 | Neo cowsay/cowthink \- configurable speaking/thinking cow (and a bit more) 32 | .SH "SYNOPSIS" 33 | .sp 34 | cowsay [\-e \fIeye_string\fP] [\-f \fIcowfile\fP] [\-h] [\-l] [\-n] [\-T \fItongue_string\fP] [\-W \fIcolumn\fP] [\-bdgpstwy] 35 | [\-\-random] [\-\-bold] [\-\-rainbow] [\-\-aurora] [\-\-super] [\fImessage\fP] 36 | .SH "DESCRIPTION" 37 | .sp 38 | \fINeo\-cowsay\fP (cowsay) generates an ASCII picture of a cow saying something provided by the 39 | user. If run with no arguments, it accepts standard input, word\-wraps 40 | the message given at about 40 columns, and prints the cow saying the 41 | given message on standard output. 42 | .sp 43 | To aid in the use of arbitrary messages with arbitrary whitespace, 44 | use the \fB\-n\fP option. If it is specified, the given message will not be 45 | word\-wrapped. This is possibly useful if you want to make the cow 46 | think or speak in figlet(6). If \fB\-n\fP is specified, there must not be any command\-line arguments left 47 | after all the switches have been processed. 48 | .sp 49 | The \fB\-W\fP specifies roughly (where the message should be wrapped. The default 50 | is equivalent to \fB\-W 40\fP i.e. wrap words at or before the 40th column. 51 | .sp 52 | If any command\-line arguments are left over after all switches have 53 | been processed, they become the cow\(cqs message. The program will not 54 | accept standard input for a message in this case. 55 | .sp 56 | There are several provided modes which change the appearance of the 57 | cow depending on its particular emotional/physical state. 58 | .sp 59 | The \fB\-b\fP option initiates Borg mode 60 | .sp 61 | \fB\-d\fP causes the cow to appear dead 62 | .sp 63 | \fB\-g\fP invokes greedy mode 64 | .sp 65 | \fB\-p\fP causes a state of paranoia to come over the cow 66 | .sp 67 | \fB\-s\fP makes the cow appear thoroughly stoned 68 | .sp 69 | \fB\-t\fP yields a tired cow 70 | .sp 71 | \fB\-w\fP is somewhat the opposite of \fB\-t\fP and initiates wired mode 72 | .sp 73 | \fB\-y\fP brings on the cow\(cqs youthful appearance. 74 | .sp 75 | The user may specify the \fB\-e\fP option to select the appearance of the cow\(cqs eyes, in which case 76 | the first two characters of the argument string \fIeye_string\fP will be used. The default eyes are \fIoo\fP. The tongue is similarly 77 | configurable through \fB\-T\fP and \fItongue_string\fP; it must be two characters and does not appear by default. However, 78 | it does appear in the \fIdead\fP and \fIstoned\fP modes. Any configuration 79 | done by \fB\-e\fP and \fB\-T\fP will be lost if one of the provided modes is used. 80 | .sp 81 | The \fB\-f\fP option specifies a particular cow picture file (\(lqcowfile\(rq) to 82 | use. If the cowfile spec contains \fI/\fP then it will be interpreted 83 | as a path relative to the current directory. Otherwise, cowsay 84 | will search the path specified in the \fBCOWPATH\fP environment variable. If \fB\-f \-\fP is specified, provides 85 | interactive Unix filter (command\-line fuzzy finder) to search the cowfile. 86 | .sp 87 | To list all cowfiles on the current \fBCOWPATH\fP, invoke \fBcowsay\fP with the \fB\-l\fP switch. 88 | .sp 89 | \fB\-\-random\fP pick randomly from available cowfiles 90 | .sp 91 | \fB\-\-bold\fP outputs as bold text 92 | .sp 93 | \fB\-\-rainbow\fP and \fB\-\-aurora\fP filters with colors an ASCII picture of a cow saying something 94 | .sp 95 | \fB\-\-super\fP ...enjoy! 96 | .sp 97 | If the program is invoked as \fBcowthink\fP then the cow will think its message instead of saying it. 98 | .SH "COWFILE FORMAT" 99 | .sp 100 | A cowfile is made up of a simple block of \fBperl(1)\fP code, which assigns a picture of a cow to the variable \fB$the_cow\fP. 101 | Should you wish to customize the eyes or the tongue of the cow, 102 | then the variables \fB$eyes\fP and \fB$tongue\fP may be used. The trail leading up to the cow\(cqs message balloon is 103 | composed of the character(s) in the \fB$thoughts\fP variable. Any backslashes must be reduplicated to prevent interpolation. 104 | The name of a cowfile should end with \fB.cow ,\fP otherwise it is assumed not to be a cowfile. Also, at\-signs (\(lq@\(rq) 105 | must be backslashed because that is what Perl 5 expects. 106 | .SH "ENVIRONMENT" 107 | .sp 108 | The COWPATH environment variable, if present, will be used to search 109 | for cowfiles. It contains a colon\-separated list of directories, 110 | much like \fBPATH or MANPATH\fP. It should always contain the \fB/usr/local/share/cows\fP 111 | directory, or at least a directory with a file called \fBdefault.cow\fP in it. 112 | .SH "FILES" 113 | .sp 114 | \fB%PREFIX%/share/cows\fP holds a sample set of cowfiles. If your \fBCOWPATH\fP is not explicitly set, it automatically contains this directory. 115 | .SH "BUGS" 116 | .sp 117 | .URL "https://github.com/Code\-Hex/Neo\-cowsay" "" "" 118 | .sp 119 | If there are any, please report bugs and feature requests in the issue tracker. 120 | Please do your best to provide a reproducible test case for bugs. This should 121 | include the \fBcowsay\fP command, the actual output and the expected output. 122 | .SH "AUTHORS" 123 | .sp 124 | Neo\-cowsay author is Kei Kamikawa (\c 125 | .MTO "x00.x7f.x86\(atgmail.com" "" ")." 126 | .sp 127 | The original author is Tony Monroe (\c 128 | .MTO "tony\(atnog.net" "" ")," 129 | with suggestions from Shannon 130 | Appel (\c 131 | .MTO "appel\(atCSUA.Berkeley.EDU" "" ")" 132 | and contributions from Anthony Polito 133 | (\c 134 | .MTO "aspolito\(atCSUA.Berkeley.EDU" "" ")." 135 | .SH "SEE ALSO" 136 | .sp 137 | perl(1), wall(1), nwrite(1), figlet(6) -------------------------------------------------------------------------------- /doc/cowsay.1.txt.tpl: -------------------------------------------------------------------------------- 1 | cowsay(1) 2 | ========= 3 | 4 | Name 5 | ---- 6 | Neo cowsay/cowthink - configurable speaking/thinking cow (and a bit more) 7 | 8 | SYNOPSIS 9 | -------- 10 | cowsay [-e _eye_string_] [-f _cowfile_] [-h] [-l] [-n] [-T _tongue_string_] [-W _column_] [-bdgpstwy] 11 | [--random] [--bold] [--rainbow] [--aurora] [--super] [_message_] 12 | 13 | DESCRIPTION 14 | ----------- 15 | _Neo-cowsay_ (cowsay) generates an ASCII picture of a cow saying something provided by the 16 | user. If run with no arguments, it accepts standard input, word-wraps 17 | the message given at about 40 columns, and prints the cow saying the 18 | given message on standard output. 19 | 20 | To aid in the use of arbitrary messages with arbitrary whitespace, 21 | use the *-n* option. If it is specified, the given message will not be 22 | word-wrapped. This is possibly useful if you want to make the cow 23 | think or speak in figlet(6). If *-n* is specified, there must not be any command-line arguments left 24 | after all the switches have been processed. 25 | 26 | The *-W* specifies roughly (where the message should be wrapped. The default 27 | is equivalent to *-W 40* i.e. wrap words at or before the 40th column. 28 | 29 | If any command-line arguments are left over after all switches have 30 | been processed, they become the cow's message. The program will not 31 | accept standard input for a message in this case. 32 | 33 | There are several provided modes which change the appearance of the 34 | cow depending on its particular emotional/physical state. 35 | 36 | The *-b* option initiates Borg mode 37 | 38 | *-d* causes the cow to appear dead 39 | 40 | *-g* invokes greedy mode 41 | 42 | *-p* causes a state of paranoia to come over the cow 43 | 44 | *-s* makes the cow appear thoroughly stoned 45 | 46 | *-t* yields a tired cow 47 | 48 | *-w* is somewhat the opposite of *-t* and initiates wired mode 49 | 50 | *-y* brings on the cow's youthful appearance. 51 | 52 | The user may specify the *-e* option to select the appearance of the cow's eyes, in which case 53 | the first two characters of the argument string _eye_string_ will be used. The default eyes are 'oo'. The tongue is similarly 54 | configurable through *-T* and _tongue_string_; it must be two characters and does not appear by default. However, 55 | it does appear in the 'dead' and 'stoned' modes. Any configuration 56 | done by *-e* and *-T* will be lost if one of the provided modes is used. 57 | 58 | The *-f* option specifies a particular cow picture file (``cowfile'') to 59 | use. If the cowfile spec contains '/' then it will be interpreted 60 | as a path relative to the current directory. Otherwise, cowsay 61 | will search the path specified in the *COWPATH* environment variable. If *-f -* is specified, provides 62 | interactive Unix filter (command-line fuzzy finder) to search the cowfile. 63 | 64 | To list all cowfiles on the current *COWPATH*, invoke *cowsay* with the *-l* switch. 65 | 66 | *--random* pick randomly from available cowfiles 67 | 68 | *--bold* outputs as bold text 69 | 70 | *--rainbow* and *--aurora* filters with colors an ASCII picture of a cow saying something 71 | 72 | *--super* ...enjoy! 73 | 74 | If the program is invoked as *cowthink* then the cow will think its message instead of saying it. 75 | 76 | COWFILE FORMAT 77 | -------------- 78 | A cowfile is made up of a simple block of *perl(1)* code, which assigns a picture of a cow to the variable *$the_cow*. 79 | Should you wish to customize the eyes or the tongue of the cow, 80 | then the variables *$eyes* and *$tongue* may be used. The trail leading up to the cow's message balloon is 81 | composed of the character(s) in the *$thoughts* variable. Any backslashes must be reduplicated to prevent interpolation. 82 | The name of a cowfile should end with *.cow ,* otherwise it is assumed not to be a cowfile. Also, at-signs (``@'') 83 | must be backslashed because that is what Perl 5 expects. 84 | 85 | ENVIRONMENT 86 | ----------- 87 | The COWPATH environment variable, if present, will be used to search 88 | for cowfiles. It contains a colon-separated list of directories, 89 | much like *PATH or MANPATH*. It should always contain the */usr/local/share/cows* 90 | directory, or at least a directory with a file called *default.cow* in it. 91 | 92 | FILES 93 | ----- 94 | *%PREFIX%/share/cows* holds a sample set of cowfiles. If your *COWPATH* is not explicitly set, it automatically contains this directory. 95 | 96 | BUGS 97 | ---- 98 | https://github.com/Code-Hex/Neo-cowsay 99 | 100 | If there are any, please report bugs and feature requests in the issue tracker. 101 | Please do your best to provide a reproducible test case for bugs. This should 102 | include the *cowsay* command, the actual output and the expected output. 103 | 104 | AUTHORS 105 | ------- 106 | Neo-cowsay author is Kei Kamikawa (x00.x7f.x86@gmail.com). 107 | 108 | The original author is Tony Monroe (tony@nog.net), with suggestions from Shannon 109 | Appel (appel@CSUA.Berkeley.EDU) and contributions from Anthony Polito 110 | (aspolito@CSUA.Berkeley.EDU). 111 | 112 | SEE ALSO 113 | -------- 114 | perl(1), wall(1), nwrite(1), figlet(6) -------------------------------------------------------------------------------- /embed.go: -------------------------------------------------------------------------------- 1 | package cowsay 2 | 3 | import ( 4 | "embed" 5 | "sort" 6 | "strings" 7 | ) 8 | 9 | //go:embed cows/* 10 | var cowsDir embed.FS 11 | 12 | // Asset loads and returns the asset for the given name. 13 | // It returns an error if the asset could not be found or 14 | // could not be loaded. 15 | func Asset(path string) ([]byte, error) { 16 | return cowsDir.ReadFile(path) 17 | } 18 | 19 | // AssetNames returns the list of filename of the assets. 20 | func AssetNames() []string { 21 | entries, err := cowsDir.ReadDir("cows") 22 | if err != nil { 23 | panic(err) 24 | } 25 | names := make([]string, 0, len(entries)) 26 | for _, entry := range entries { 27 | name := strings.TrimSuffix(entry.Name(), ".cow") 28 | names = append(names, name) 29 | } 30 | sort.Strings(names) 31 | return names 32 | } 33 | 34 | var cowsInBinary = AssetNames() 35 | 36 | // CowsInBinary returns the list of cowfiles which are in binary. 37 | // the list is memoized. 38 | func CowsInBinary() []string { 39 | return cowsInBinary 40 | } 41 | -------------------------------------------------------------------------------- /examples/basic/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Code-Hex/Neo-cowsay/v2/example 2 | 3 | go 1.16 4 | 5 | require github.com/Code-Hex/Neo-cowsay/v2 v2.0.1 6 | 7 | replace github.com/Code-Hex/Neo-cowsay/v2 => ../../ 8 | -------------------------------------------------------------------------------- /examples/basic/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Code-Hex/go-wordwrap v1.0.0 h1:yl5fLyZEz3+hPGbpTRlTQ8mQJ1HXWcTq1FCNR1ch6zM= 2 | github.com/Code-Hex/go-wordwrap v1.0.0/go.mod h1:/SsbgkY2Q0aPQRyvXcyQwWYTQOIwSORKe6MPjRVGIWU= 3 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 4 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 5 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 6 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 7 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 8 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 9 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 10 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 11 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 12 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 13 | -------------------------------------------------------------------------------- /examples/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | cowsay "github.com/Code-Hex/Neo-cowsay/v2" 7 | ) 8 | 9 | func main() { 10 | if false { 11 | simple() 12 | } else { 13 | complex() 14 | } 15 | } 16 | 17 | func simple() { 18 | say, err := cowsay.Say( 19 | "Hello", 20 | cowsay.Type("default"), 21 | cowsay.BallonWidth(40), 22 | ) 23 | if err != nil { 24 | panic(err) 25 | } 26 | fmt.Println(say) 27 | } 28 | 29 | func complex() { 30 | cow, err := cowsay.New( 31 | cowsay.BallonWidth(40), 32 | //cowsay.Thinking(), 33 | cowsay.Random(), 34 | ) 35 | if err != nil { 36 | panic(err) 37 | } 38 | say, err := cow.Say("Hello") 39 | if err != nil { 40 | panic(err) 41 | } 42 | fmt.Println(say) 43 | } 44 | -------------------------------------------------------------------------------- /examples/echo-server/README.md: -------------------------------------------------------------------------------- 1 | ## echo server 2 | 3 | ``` 4 | $ go build 5 | $ ./echo-server 6 | 2021/11/14 16:16:27 server addr => 127.0.0.1:54456 7 | ``` 8 | 9 | 10 | ## client 11 | 12 | Use [netcat](https://en.wikipedia.org/wiki/Netcat) 13 | 14 | Please enter `^D` (control + D) after you enter any characters. 15 | 16 | ``` 17 | $ nc 127.0.0.1 54456 18 | hello 19 | _______ 20 | < hello > 21 | ------- 22 | \ ^__^ 23 | \ (oo)\_______ 24 | (__)\ )\/\ 25 | ||----w | 26 | || || 27 | ``` 28 | -------------------------------------------------------------------------------- /examples/echo-server/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Code-Hex/Neo-cowsay/v2/examples/echo-server 2 | 3 | go 1.16 4 | 5 | require github.com/Code-Hex/Neo-cowsay/v2 v2.0.1 6 | 7 | replace github.com/Code-Hex/Neo-cowsay/v2 => ../../ 8 | -------------------------------------------------------------------------------- /examples/echo-server/go.sum: -------------------------------------------------------------------------------- 1 | github.com/Code-Hex/go-wordwrap v1.0.0 h1:yl5fLyZEz3+hPGbpTRlTQ8mQJ1HXWcTq1FCNR1ch6zM= 2 | github.com/Code-Hex/go-wordwrap v1.0.0/go.mod h1:/SsbgkY2Q0aPQRyvXcyQwWYTQOIwSORKe6MPjRVGIWU= 3 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 4 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 5 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 6 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 7 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 8 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 9 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 10 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 11 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 12 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 13 | -------------------------------------------------------------------------------- /examples/echo-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "log" 7 | "net" 8 | "os" 9 | "os/signal" 10 | "strings" 11 | "time" 12 | 13 | cowsay "github.com/Code-Hex/Neo-cowsay/v2" 14 | ) 15 | 16 | const limit = 1000 17 | 18 | func main() { 19 | ln, err := net.Listen("tcp", "127.0.0.1:0") 20 | if err != nil { 21 | log.Fatal(err) 22 | } 23 | log.Println("server addr =>", ln.Addr()) 24 | 25 | ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) 26 | defer cancel() 27 | 28 | go func() { 29 | for { 30 | conn, err := ln.Accept() 31 | if err != nil { 32 | log.Println(err) 33 | return 34 | } 35 | conn.SetDeadline(time.Now().Add(5 * time.Second)) 36 | 37 | go func() { 38 | defer conn.Close() 39 | go func() { 40 | <-ctx.Done() 41 | // cancel 42 | conn.SetDeadline(time.Now()) 43 | }() 44 | 45 | var buf strings.Builder 46 | rd := io.LimitReader(conn, limit) 47 | if _, err := io.Copy(&buf, rd); err != nil { 48 | log.Println("error:", err) 49 | return 50 | } 51 | 52 | phrase := strings.TrimSpace(buf.String()) 53 | log.Println(phrase) 54 | say, _ := cowsay.Say(phrase) 55 | conn.Write([]byte(say)) 56 | }() 57 | } 58 | }() 59 | 60 | <-ctx.Done() 61 | } 62 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Code-Hex/Neo-cowsay/v2 2 | 3 | require ( 4 | github.com/Code-Hex/go-wordwrap v1.0.0 5 | github.com/google/go-cmp v0.5.6 6 | github.com/mattn/go-runewidth v0.0.13 7 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 8 | ) 9 | 10 | go 1.16 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Code-Hex/go-wordwrap v1.0.0 h1:yl5fLyZEz3+hPGbpTRlTQ8mQJ1HXWcTq1FCNR1ch6zM= 2 | github.com/Code-Hex/go-wordwrap v1.0.0/go.mod h1:/SsbgkY2Q0aPQRyvXcyQwWYTQOIwSORKe6MPjRVGIWU= 3 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 4 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 5 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 6 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 7 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 8 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 9 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 10 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 11 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 12 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 13 | -------------------------------------------------------------------------------- /split.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package cowsay 5 | 6 | import "strings" 7 | 8 | func splitPath(s string) []string { 9 | return strings.Split(s, ":") 10 | } 11 | -------------------------------------------------------------------------------- /split_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package cowsay 5 | 6 | import "strings" 7 | 8 | func splitPath(s string) []string { 9 | return strings.Split(s, ";") 10 | } 11 | -------------------------------------------------------------------------------- /testdata/default.cow: -------------------------------------------------------------------------------- 1 | ________ 2 | < hello! > 3 | -------- 4 | \ ^__^ 5 | \ (oo)\_______ 6 | (__)\ )\/\ 7 | ||----w | 8 | || || -------------------------------------------------------------------------------- /testdata/nest.cow: -------------------------------------------------------------------------------- 1 | ______________________________ 2 | / ________ \ 3 | | < cowsay > | 4 | | -------- | 5 | | \ ^__^ | 6 | | \ (oo)\_______ | 7 | | (__)\ )\/\ | 8 | | ||----w | | 9 | \ || || / 10 | ------------------------------ 11 | \ ^__^ 12 | \ (oo)\_______ 13 | (__)\ )\/\ 14 | ||----w | 15 | || || -------------------------------------------------------------------------------- /testdata/testdir/test.cow: -------------------------------------------------------------------------------- 1 | $the_cow = <