Starting with Rust

Apr. 30, 2022

TLDR: I finally bit the bullet and jumped into Rust and I love it.

Meet Todoist CLI

I am a big user of todoist for everyday task tracking. It’s simple and easy to use, but has one small problem. I can’t do it from my terminal. So I had started to write my own CLI app in Go for that, since I was comfortable with it. It was basically just a wrapper around their REST API. I had written the most basic things already like creating, listing and closing tasks, so I hadn’t done anything much yet.

This was a great way how to try making something in Rust without spending all my time just trying to find a problem to solve.

Link to the GitHub project

Writing the CLI

When writing CLI apps in Go I use urfave/cli for small and very simple projects or bust out spf13/cobra for something bigger.

The only thing that I found that is comparable in Rust is clap which actually works great, but is not really a toolkit for building CLI apps, but an arguments parser.

The good thing is that it was pretty simple to start working on, even though learning how macros work and all the involved magic with them is kinda confusing at start.

But the more I used it the more I loved it. The way how I can leverage enums to create commands and subcommands just feels so smooth. And using them is also feels like magic.

Just look at the example:

#[derive(Subcommand, Debug)]
enum Commands {
    /// Work with tasks
    Tasks(Tasks),
}

#[derive(Debug, Args)]
struct Tasks {
    #[clap(subcommand)]
    command: TaskCommands,
}

#[derive(Debug, Subcommand)]
enum TaskCommands {
    /// List commands, default to today and overdue
    List {
        #[clap(long, short)]
        filter: Option<String>,
    },
    /// Create a task
    Create {
        /// Content of the task
        content: Option<String>,
        /// Tasks due date
        due: Option<String>,
        /// Tasks project
        project: Option<String>,
    },
}

Creates all that is needed to parse arguments and flags from the command line. The comments are used for help text and it looks awesome:

▶ todoist --help
todoist 0.1.0

USAGE:
    todoist <SUBCOMMAND>

OPTIONS:
    -h, --help       Print help information
    -V, --version    Print version information

SUBCOMMANDS:
    help        Print this message or the help of the given subcommand(s)
    tasks       Work with tasks

▶ todoist tasks --help  
Work with tasks

USAGE:
    todoist tasks <SUBCOMMAND>

OPTIONS:
    -h, --help    Print help information

SUBCOMMANDS:
    create    Create a task
    help      Print this message or the help of the given subcommand(s)
    list      List commands, default to today and overdue

And then you can easily parse this and work with the options using match statements. And it also doesn’t let you compile if you forget to match something and it’s so empowering. I don’t have to think about all the options, I just fix what the compiler doesn’t like.

fn main() {
    let cli = Cli::parse();
    let theme = ColorfulTheme::default();
    match &cli.command {
        Commands::Tasks(tasks) => match &tasks.command {
            TaskCommands::List { filter } => {}
            TaskCommands::Create { content, due, project } => {},
            TaskCommands::Done { id } => {},
            TaskCommands::View { id } => {},
        },
    }
    Ok(())
}

Problem with structure

To preface this, my dev work started out writing different PHP applications and some Python scripts, and then I moved into Go development space. I have dabbled with Java, C#, JS/TS and some other language that I forget. All of those language have fairly straight forward way how to deal with structuring your project:

So the first thing I tried with Rust is the same way I do it in go. I just created a different file in the same directory and tried to use something from it. This doesn’t work. Right away.

▶ ls --tree
.
├── foo
│  └── file.rs
└── main.rs

main.rs

use foo;

fn main() {
    bar()
}

foo/file.rs

pub fn bar() {
    println!("bar");
}

But this shows an error:

error[E0432]: unresolved import `foo`
 --> src/main.rs:1:5
  |
1 | use foo;
  |     ^^^ no external crate `foo`

error[E0425]: cannot find function `bar` in this scope
 --> src/main.rs:4:5
  |
4 |     bar()
  |     ^^^ not found in this scope

Some errors have detailed explanations: E0425, E0432.
For more information about an error, try `rustc --explain E0425`.
error: could not compile `folders` due to 2 previous errors

This is because of the way how Rust imports work is a bit different. There are bunch of ways how to do this. Either create a foo/mod.rs file. In that file then have to declare the foo/file.rs as a public module.

src/foo/mod.rs

pub mod file;

Then you can use it in main as a module:

main.rs:

mod foo;

fn main() {
    foo::file::bar()
}

And then there is the crate syntax with the use keyword that also has crate and self and super keywords, but for more info on that the Rust book will do a lot better job then I ever would.

Final thoughts

I really am loving Rust so far. The type system is awesome. The compiler is awesome(although slow, Go has spoiled me). But there is a lot to learn and curve is steep.

But this small project has convinced me that my CLI tools from now on are gonna be in Rust.