diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..765d1ad --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,41 @@ +# .github/workflows/release.yml +name: goreleaser + +on: + pull_request: + push: + # run only against tags + tags: + - "*" + +permissions: + contents: write + # packages: write + # issues: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: stable + # More assembly might be required: Docker logins, GPG, etc. + # It all depends on your needs. + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + # either 'goreleaser' (default) or 'goreleaser-pro' + distribution: goreleaser + # 'latest', 'nightly', or a semver + version: "~> v2" + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # Your GoReleaser Pro key, if you are using the 'goreleaser-pro' distribution + # GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0bc0f5a --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +lil-guy diff --git a/README.md b/README.md new file mode 100644 index 0000000..6c82efa --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# lil-guy + +lil-guy is a fun, customizable command-line animation tool that displays cute characters with messages and can output from stdin. + + + +## Features + +- Multiple pre-defined characters +- Support for custom characters via TOML configuration +- Multi-line character support +- Animated character display +- Customizable messages +- Debug mode for troubleshooting + +## Installation + +### Releases + +Prebuilt binaries can be found in [releases](https://github.com/fvckgrimm/lil-guy/releases) + +### Build from source + +1. Clone the repository + +```bash +git clone https://github.com/fvckgrimm/lil-guy.git && cd lil-guy +``` + +2. Build the project + +```bash +go build -v -o lil-guy +``` + +## Configuration + +lil-guy uses TOML file for character configuration, the default location which it's read from is: + +``` +~/.config/lil-guy/characters.toml +``` + +## Usage + +Run lil-guy with the following command: + +```bash +./lil-guy [flags] +``` + +Available flags: + +* -message: Set the message to display (default: "Hello, I'm lil guy!") +* -character: Choose the character to display (default: "default") +* -debug: Run in debug mode for troubleshooting + +Examples: + +```bash +./lil-guy -message "Hello, World!" -character cat +./lil-guy -message "I'm a dog" -character dog +./lil-guy -message "Multi-line test" -character multiline_example +./lil-guy -debug -character fumo_1 +``` + +You can also pipe other commands into lil-guy: + +```bash +pip install discord | lil-guy -message "I can handle this myself..." -character blinkcat +``` + + +## Adding New Characters + +To add a new character, simply add a new entry to your `characters.toml` file. No changes to the source code are required. + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. + diff --git a/assets/showcase.gif b/assets/showcase.gif new file mode 100644 index 0000000..2927d9c Binary files /dev/null and b/assets/showcase.gif differ diff --git a/example-characters.toml b/example-characters.toml new file mode 100644 index 0000000..a59b4c2 --- /dev/null +++ b/example-characters.toml @@ -0,0 +1,91 @@ +[default] +faces = ["(o_o)"] + +[cat] +faces = ["(^._.^)"] + +[dog] +faces = ["(ᵔᴥᵔ)"] + +[fumo_1] +faces = ["ᗜˬᗜ", "ᗜ˰ᗜ"] + +[fumo_2] +faces = ["ᗜ ̫ ᗜ"] + +[fumo_3] +faces = ["ᗜ‿ᗜ"] + +[x] +faces = ["X‿X", "-‿X", "X‿-"] + +[zero] +faces = ["0x0", "-x-", "0x0"] + +[happyCat] +faces = [ +''' + /\_/\ +( o.o ) + > ^ < +''', +''' + /\_/\ +( ^.^ ) + > ^ < +''' +] + +[dance_cat] +faces = [ +''' + 彡 ⌒ ミ + (´・ω・`) )) +(( ( つ ヽ、 +  〉 とノ ))) + (__ノ^(_) +''', +''' +  彡 ⌒ ミ + (´・ω・`) + γ  と ) )) +((( ヽつ 〈 + (_)^(__) +''' +] + +[dance_cat1] +faces = [ +''' +    彡⌒ ミ      ♪ +   (´・ω・`)  ♪ +   ( つ つ + (( (⌒ __) )) +    し' っ +''', +''' +  ♪  彡⌒ ミ +    ∩´・ω・`)   ♪ +    ヽ ⊂ノ +   (( (  ⌒)  )) +     c し' +''' +] + +[dance_cat2] +faces = [ +''' +    ∧_∧   ♪ +    (´・ω・`∩ +     o   ,ノ +    O_ .ノ +♪      (ノ +''', +''' +    ∧_∧ ♪ +    ∩・ω・`) +    |   ⊂ノ +   |   _⊃  ♪ +    し ⌒ +''' +] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..19ef702 --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module github.com/fvckgrimm/lil-guy + +go 1.23.0 + +require ( + github.com/BurntSushi/toml v1.4.0 + github.com/fatih/color v1.17.0 +) + +require ( + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + golang.org/x/sys v0.18.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..17f5c1c --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/main.go b/main.go new file mode 100644 index 0000000..a2017c5 --- /dev/null +++ b/main.go @@ -0,0 +1,198 @@ +package main + +import ( + "bufio" + "flag" + "fmt" + "log" + "os" + "os/signal" + "path/filepath" + "strings" + "syscall" + "time" + + "github.com/BurntSushi/toml" + "github.com/fatih/color" +) + +type Character struct { + Faces []string `toml:"faces"` +} + +type Config struct { + Characters map[string]Character +} + +const outputLines = 10 // Number of output lines to display + +func main() { + message := flag.String("message", "Hello, I'm lil guy!", "Message to display") + characterName := flag.String("character", "default", "Character to use") + debug := flag.Bool("debug", false, "Run in debug mode") + flag.Parse() + + config, err := loadConfig() + if err != nil { + fmt.Printf("Error loading config: %v\n", err) + os.Exit(1) + } + + if *debug { + fmt.Printf("Loaded characters: %v\n", config.Characters) + fmt.Printf("Requested character: %s\n", *characterName) + } + + character, ok := config.Characters[*characterName] + if !ok { + if *debug { + fmt.Printf("Character '%s' not found. Using default.\n", *characterName) + } + character = config.Characters["default"] + } + + if *debug { + fmt.Printf("Selected character: %v\n", character) + fmt.Println("Press Enter to continue...") + bufio.NewReader(os.Stdin).ReadBytes('\n') + } + + if len(character.Faces) == 0 { + fmt.Printf("No faces defined for character '%s'. Using default.\n", *characterName) + character = config.Characters["default"] + } + + log.Printf("Selected character: %v\n", character) + + // If default character is also empty, use a fallback + if len(character.Faces) == 0 { + character.Faces = []string{"(o_o)"} + } + + frames := []string{"<", "-", ">", " "} + + // Clear screen and hide cursor + fmt.Print("\033[2J\033[H\033[?25l") + defer fmt.Print("\033[?25h") // Show cursor when done + + c := color.New(color.FgCyan) + + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + outputChan := make(chan string) + go readStdin(outputChan) + + go func() { + <-sigChan + fmt.Print("\033[?25h") // Show cursor before exiting + os.Exit(0) + }() + + characterIndex := 0 + outputBuffer := make([]string, outputLines) + for i := range outputBuffer { + outputBuffer[i] = "" // Initialize with empty strings + } + + for { + for _, frame := range frames { + // Move cursor to top-left and print animation + fmt.Print("\033[H") + + lines := strings.Split(character.Faces[characterIndex], "\n") + isMultiLine := len(lines) > 1 + + for i, line := range lines { + if i == 0 && !isMultiLine { + // Only apply arms to single-line characters + leftArm, rightArm := getArms(frame) + c.Printf(" %s %s %s\n", leftArm, line, rightArm) + } else { + // For multi-line characters or subsequent lines, don't add arms + c.Printf(" %s\n", line) + } + } + + fmt.Printf("\n %s\n\n", *message) + + // Display the last few lines of output + for _, line := range outputBuffer { + if line != "" { + fmt.Printf(" %s\n", line) + } + } + + // Add padding to cover any previous longer messages + padding := strings.Repeat(" ", 50) + fmt.Printf("%s\n", padding) + + time.Sleep(250 * time.Millisecond) + + // Check for new output + select { + case newOutput := <-outputChan: + // Shift the buffer and add the new output + outputBuffer = append(outputBuffer[1:], newOutput) + default: + // No new output, continue with the current buffer + } + } + + // Cycle to the next face for the character + characterIndex = (characterIndex + 1) % len(character.Faces) + } +} + +func loadConfig() (*Config, error) { + home, err := os.UserHomeDir() + if err != nil { + return nil, err + } + + configPath := filepath.Join(home, ".config", "lil-guy", "characters.toml") + fmt.Printf("Loading config from: %s\n", configPath) + + // Read and print raw file contents + rawContent, err := os.ReadFile(configPath) + if err != nil { + return nil, fmt.Errorf("error reading config file: %v", err) + } + fmt.Println("Raw config file contents:") + fmt.Println(string(rawContent)) + + var config Config + config.Characters = make(map[string]Character) + + _, err = toml.DecodeFile(configPath, &config.Characters) + if err != nil { + return nil, fmt.Errorf("error decoding TOML: %v", err) + } + + fmt.Printf("Loaded config: %+v\n", config) + + // Ensure default character exists + if _, ok := config.Characters["default"]; !ok { + config.Characters["default"] = Character{Faces: []string{"(o_o)"}} + } + + return &config, nil +} + +func readStdin(outputChan chan<- string) { + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + outputChan <- scanner.Text() + } +} + +func getArms(frame string) (string, string) { + switch frame { + case "<": + return "<", "<" + case ">": + return ">", ">" + default: + return frame, frame + } +}