Convert "sl" Source Code from C to Rust

Click here for complete code up to this point
Previous = Alternative String-Trimming Methods

Get More User-provided Options

In previous lessons, most notably Get User-provided Options From the Command-line, we provided a means for the user to specify which image of several to animate. But we need to be able to get several more functions, such as:

Let's Fly!

Let's start by adding the option to fly. We'll have to make a change to our "Args" struct, and make relevant changes to the main loop in the "main()" function.

main.rs
    
...
/* Define the inputs we might expect on the command-line. */
struct Args {
  /// Which image 'D51', 'C51', 'Jack', 'Boat', 'Plane', 'Twinengine', 'Little', 'Motorcycle', or 'Ferris'?
  #[arg(short, long, default_value_t = String::from("D51"))]
  // Tells clap that the next line is an argument, that can be entered in as "k" or "kind".
  kind: String,
  
  /// Fly?
  #[arg(short, long, default_value_t = false)]
  fly: bool,
  
} // end of Args definitions
...
  let mut row: i32 = screen_height - height;
  
    let distance_to_travel = image_width + screen_width;
    for col in (0..distance_to_travel).rev() {
        let frame_num = (col % num_of_frames) as usize; // The remainder of this math tells us which frame to display, in descending order.
        let frame = &image_vecvec[frame_num];
        draw_frame(row, col - image_width, frame);
        // If flying, then fly up only every x'th frame.
        let fly_upward_per_how_many_frames = 20;
        if switches.fly && (col % fly_upward_per_how_many_frames == 0) {
            row -= 1; // Fly up one row.
            // If we go off the top of screen,
            if row < -image_height {
                row = screen_height; // start over at bottom.
            }
        }
        thread::sleep(delay_between_frames);
    }
...

Note that "fly" is of type boolean, rather than String. What this means in practice is that if the option is provided on the command-line, then it's true, and if it's not provided, then it's false. This means that instead of having a command that looks like this:

$ cargo run -- --fly = true

we can have one that looks like this:

$ cargo run -- --fly

It saves the user a bit of typing. Sweet!

When I chose to fly the Twin-Engine ($ cargo run -- --fly --kind=twinengine), I discovered that it leaves behind an unwanted trail of plane pieces. To fix this, let's erase the bottom-most line of the previous frame:

main.rs
...
fn draw_frame(mut row: i32, col: i32, frame: &Vec<String>) {
    for each_line in frame {
        my_mvaddstr(row, col, &each_line);
        row += 1;
    }
    // Also erase the bottom line of the previous frame (to erase left-over pieces if we're flying).
    // Create a line with as many space characters as the image is wide.
    let erasure_line = " ".repeat(frame[0].len());
    // "row" should currently be pointing one row beneath current frame. Erase it.
    my_mvaddstr(row, col, &erasure_line);
    refresh();
} // end of draw_frame()
...

Full-Speed Ahead!

Or quarter-speed. Or a crawl. Whatever.

main.rs
    
...
/* Define the inputs we might expect on the command-line. */
struct Args {
    /// Which image 'D51', 'C51', 'Jack', 'Boat', 'Plane', 'Twinengine', 'Little', 'Motorcycle', or 'Ferris'?
    #[arg(short, long, default_value_t = String::from("D51"))]
    // Tells clap that the next line is an argument, that can be entered in as "k" or "kind".
    kind: String,
  
    /// Fly?
    #[arg(short, long, default_value_t = false)]
    fly: bool,

    /// Speed of animation? '[c]rawl', '[s]low', '[n]ormal', '[f]ast', '[z]ippy', '[[u]ltra]fast', '[z]oom', '[2]fast2see' [default: NORMAL]
    #[arg(short, long, default_value_t = String::from("N"))]
    speed: String,

} // end of Args definitions

fn main() {
    // Get command-line options using Clap.
    let switches: Args = Args::parse();

    // This will be our delay, in milliseconds, between frame images.
    let mut delay = 50;
    
    match (&switches.speed.to_uppercase()).as_str() {
        "C" | "CRAWL" => delay = 400,
        "S" | "SLOW" => delay = 200,
        "N" | "NORMAL" => delay = 50,
        "F" | "FAST" => delay = 30,
        "Z" | "ZIPPY" => delay = 20,
        "U" | "ULTRA" | "ULTRAFAST" => delay = 10,
        "ZOOM" => delay = 1,
        "2" | "2fast2see" => delay = 0,
        _ => delay = delay,
    }
    let delay_between_frames = time::Duration::from_millis(delay);

    // Initialize the ncurses screen.
...

At this point, you can run the program with a command like one of the following:

cargo run -- --speed zippy --fly
cargo run -- -f
cargo run -- -s f -f
cargo run -- --kind=plane --speed fast -fly
cargo run -- --kind=plane -s f -f
cargo run -- -sz -f -ktwin
cargo run -- --fly --speed ultrafast
cargo run -- --help

By the way, if for some reason you wanted to use different letters/words for the options, say, "t" and "takeoff" instead of "f" and "fly", you could write the "arg" line like this: #[arg(short = "t", long = "takeoff")].

You can also change the value of the option in the Help screen like this: #[arg(short, long, default_value_t = String::from("N"), value_name="Velocity, Baby, yeah!")], which would generate this:

$ cargo run -q -- -h

Usage: sl [OPTIONS]

Options:
  -k, --kind <KIND>                    Which image 'D51', 'C51', 'Jack', 'Boat', 'Plane', 'Twinengine', 'Little', 'Motorcycle', or 'Ferris'? [default: D51]
  -f, --fly                            Fly?
  -s, --speed <Velocity, Baby, yeah!>  Speed of animation? '[c]rawl', '[s]low', '[n]ormal', '[f]ast', '[z]ippy', '[[u]ltra]fast', '[z]oom', '[2]fast2see' [default: NORMAL] [default: N]
  -h, --help                           Print help
$ 

And when entering your program arguments, the following are all identical:

$ cargo run -- -s f
$ cargo run -- -sf
$ cargo run -- -s=f
$ cargo run -- -s fast
$ cargo run -- -sfast
$ cargo run -- -s=fast
$ cargo run -- --speed=fast
$ cargo run -- --speed fast

(But "--speedf" and "--speedfast" won't work.)

And just as a reminder, cargo run -- -s f is equivalent to ./target/debug/sl -s f, because when you do a "cargo run", cargo is compiling your source code into a binary executable named "sl" which it places into "./target/debug", and then running that. You can run either command to get the same result.

Be aware that if you don't provide a default for an option (using the default_value_t bit), the option is required to be entered on the command-line.

Affixing the Coalcar

main.rs
/* Define the argument inputs we might expect; put them in a struct named "Args". */
struct Args {
    /// Which image 'D51', 'C51', 'Jack', 'Boat', 'Plane', 'Twinengine', 'Little', 'Motorcycle', or 'Ferris'?
    #[arg(short, long, default_value_t = String::from("D51"))]
    // Tells clap that the next line is an argument, that can be entered in as "k" or "kind".
    kind: String,

    /// Fly?
    #[arg(short, long, default_value_t = false)]
    fly: bool,

    /// Speed of animation? '[c]rawl', '[s]low', '[n]ormal', '[f]ast', '[z]ippy', '[[u]ltra]fast', '[z]oom', '[2]fast2see' [default: NORMAL]
    #[arg(short, long, default_value_t = String::from("N"), value_name="Velocity, Baby, yeah!")]
    speed: String,

    /// Leave off the coalcar?
    #[arg(short, long, default_value_t = false)]
    no_trailer: bool,
} // end of Args definition
main.rs
//██████████████████████████ get_image() ██████████████████████████
//█                                                               █
fn get_image(kind: &str) -> Vec<Vec<String>> {
    // Convert "kind" to upper-case.
    let upcased_kind = &kind.to_uppercase() as &str;

    // Our string is stored in a constant in the "images.rs" file.
    let const_str: &str = match upcased_kind {
        "D" | "D51" => D51,
        "L" | "LITTLE" => LITTLE,
        "C" | "C51" => C51,
        "B" | "BOAT" => BOAT,
        "T" | "TWINENGINE" | "TWIN" => TWINENGINE,
        "P" | "PLANE" => PLANE,
        "M" | "MOTORCYCLE" => MOTORCYCLE,
        "J" | "JACK" => JACK,
        "F" | "FERRIS" | "MASCOT" => FERRIS,
        _ => D51, // The default.
    };

    //The first character in the string, after the 'r"', is a newline; lose it.
    let image_string = &const_str[1..const_str.len()];

    // Create new blank vector of vector of Strings.
    let mut outer_vec: Vec<Vec<String>> = Vec::new();

    // This keeps track of which frame/inner vector we're processing.
    let mut inner_vec_num: usize = 0;

    // Create first inner vector in the outer vector.
    outer_vec.push(Vec::new());

    for mut each_line in image_string.lines() {
        if each_line != "" {
            // If a blank line is not found ...
            // ... remove single-quote mark at end of each line,
            if &each_line[each_line.len() - 1..] == "'" {
                each_line = &each_line[0..each_line.len() - 1];
            }
            // Convert 'each_line' to a String.
            let mut line = each_line.to_string();
            // ... and make sure last column of each line is a blank space ...
            line.push(' ');
            // ... and then add the string to the current inner vector.
            outer_vec[inner_vec_num].push(line);
        } else {
            // If a blank line is found...
            // ... we're done with the current vector, so create a new inner vector...
            outer_vec.push(Vec::new());
            // ... and increase the count of vectors by one.
            inner_vec_num += 1;
        }
    }
    
    // We may need to add the coalcar to the D51 or C51 trains.
    outer_vec = if !switches.no_trailer && ((upcased_kind == "D51") || (upcased_kind == "C51")) {
        let coalcar_vecvec = get_image(&"coalcar");
        outer_vec = merge_vecs(outer_vec, coalcar_vecvec);
        outer_vec
    } else {
      outer_vec
    };
     
    // Return the finished vector of String vectors.
    outer_vec
} // end of get_image()

//█                                                               █
//████████████████████ end of get_image() █████████████████████████


//███████████████████████ end of merge_vecs() █████████████████████
//█                                                               █
fn merge_vecs(vec1: Vec<Vec<String>>, vec2: Vec<Vec<String>>) -> Vec<Vec<String>> {
  vec2 // We'll flesh this function out in a bit.
  } // end of merge_vecs()

//█                                                               █
//███████████████████████ end of merge_vecs() █████████████████████

Accident?

We can add "accident" (which I'll call "oops") in a similar fashion. In "src/main.rs":

main.rs
  let (speed, fly, kind, oops) = parse_opts(switches);

Also in "src/main.rs", we will need to feed this option to the "draw_d51()" function, so it can draw the accident features.

main.rs
fn main() {
    let (speed, fly, kind, oops) = parse_opts();
    draw_d51(speed, fly, oops);
} // end of main()

And in "src/parsing.rs":

parsing.rs
/* Define the inputs we might expect on the command-line. */
struct Args {

  /// How fast to move the object? ultrafast | zippy | fast | medium | slow | crawl
  #[arg(short, long, default_value_t = String::from("med"))]
  speed: String,
  
  /// Fly?
  #[arg(short, long, default_value_t = false)]
  fly: bool,

  /// What Kind of object? D51 | C51 | Little | Jack | Boat | Plane
  #[arg(short, long, default_value_t = String::from("D51"))]
  kind: String,
  
  /// Oops?
  #[arg(short, long, default_value_t = false)]
  oops: bool,
  
} // end of Args definitions


pub fn parse_opts() -> (u64, bool, String, bool) {
...
...  
...
  return (speed, switches.fly, kind, switches.oops)
} // end of parse_opts()   

And in "src/d51.rs":

d51.rs
...
pub fn draw_d51(delay: u64, fly: bool, oops: bool) {
...

When you compile this, the compiler will complain about all the unused variables, but the program should compile and run as it did before.