DEV Community

justhiro
justhiro

Posted on

Building a Decision-Making CLI Tool in Rust: "should-i"

Introduction

"What should I have for lunch?" "Should I try this new library?" From daily small decisions to technical choices, we face countless decisions every day. Why not let the universe decide for you?

I built a CLI tool called "should-i" that helps you make decisions by consulting the yesno.wtf API. In this article, I'll share the implementation details and technical insights.

What is should-i?

should-i is a simple CLI tool that answers your questions with YES/NO/MAYBE. It also returns a GIF image URL, making the experience more engaging and visual.

Basic Usage

should-i in action

Browser GIF Display with --open Option

With the --open option, the tool automatically opens the response GIF in your browser:

$ should-i "take a coffee break" --open
🎲 Asking the universe...
βœ… YES! Do it! πŸŽ‰
πŸ–ΌοΈ https://yesno.wtf/assets/yes/11-a23cbde4ae018bbda812d2d8b2b8fc6c.gif
# Browser opens with the GIF automatically
Enter fullscreen mode Exit fullscreen mode

Tech Stack

This project uses the following Rust crates:

  • reqwest: HTTP client
  • tokio: Async runtime
  • serde/serde_json: JSON serialization/deserialization
  • clap: CLI argument parser
  • anyhow: Error handling
  • webbrowser: Browser opening functionality

Implementation Highlights

1. API Communication

The yesno.wtf API is a simple RESTful API with responses structured like this:

{
  "answer": "yes",
  "forced": false,
  "image": "https://yesno.wtf/assets/yes/2.gif"
}
Enter fullscreen mode Exit fullscreen mode

We map this to a Rust struct:

#[derive(Deserialize)]
struct YesNoResponse {
    answer: String,
    image: String,
}
Enter fullscreen mode Exit fullscreen mode

Making async HTTP requests with reqwest is simple and type-safe:

let response = client
    .get("https://yesno.wtf/api")
    .send()
    .await?
    .json::()
    .await?;
Enter fullscreen mode Exit fullscreen mode

2. CLI Interface Design

Using clap makes implementing an intuitive CLI straightforward. The derive macro approach is very Rust-like, automatically generating a parser from struct definitions:

#[derive(Parser)]
#[command(name = "should-i")]
#[command(about = "A CLI tool to help you make decisions")]
struct Cli {
    /// The question you want to ask
    question: String,

    /// Open the GIF image in your browser
    #[arg(short, long)]
    open: bool,
}
Enter fullscreen mode Exit fullscreen mode

3. Enhancing User Experience

Rather than just displaying results, I used emojis to make the output visually clear and engaging:

match response.answer.as_str() {
    "yes" => println!("βœ… YES! Do it! πŸŽ‰"),
    "no" => println!("❌ NO! Don't do it! 🚫"),
    "maybe" => println!("πŸ€” MAYBE... You decide! 🎲"),
    _ => println!("🀷 Unknown response"),
}
Enter fullscreen mode Exit fullscreen mode

The --open option automatically opens the GIF in your browser:

if cli.open {
    if let Err(e) = webbrowser::open(&response.image) {
        eprintln!("⚠️  Failed to open browser: {}", e);
    }
}
Enter fullscreen mode Exit fullscreen mode

This feature lets you visually enjoy the universe's message, which is one of the tool's charms.

Error Handling

Leveraging Rust's powerful type system, I implemented proper error handling using the anyhow crate:

async fn fetch_decision() -> anyhow::Result {
    let client = reqwest::Client::new();
    let response = client
        .get("https://yesno.wtf/api")
        .send()
        .await
        .context("Failed to connect to yesno.wtf API")?
        .json::()
        .await
        .context("Failed to parse API response")?;

    Ok(response)
}
Enter fullscreen mode Exit fullscreen mode

The context() method provides specific context when errors occur.

Project Structure

should-i/
β”œβ”€β”€ Cargo.toml          # Dependencies and metadata
β”œβ”€β”€ src/
β”‚   └── main.rs         # Main implementation
β”œβ”€β”€ LICENSE-MIT
β”œβ”€β”€ LICENSE-APACHE
└── README.md
Enter fullscreen mode Exit fullscreen mode

As a simple tool, everything is contained in a single file. For larger projects, consider splitting into modules.

Installation

Using Cargo

cargo install should-i
Enter fullscreen mode Exit fullscreen mode

Using Homebrew

brew tap Justhiro55/tap
brew install should-i
Enter fullscreen mode Exit fullscreen mode

From Source

git clone https://github.com/Justhiro55/should-i
cd should-i
cargo install --path .
Enter fullscreen mode Exit fullscreen mode

Conclusion

should-i is simple, but it contains fundamental elements of Rust CLI development:

  • Async HTTP communication
  • JSON parsing
  • CLI argument handling
  • Error handling
  • Cross-platform support

The playful concept of "letting the universe decide" made implementing practical Rust techniques enjoyable.

When in doubt, just ask should-i!

$ should-i "write more Rust code"
🎲 Asking the universe...
βœ… YES! Do it! πŸŽ‰
Enter fullscreen mode Exit fullscreen mode

Links

Top comments (0)