Convert "sl" Source Code from C to Rust

Previous = Get User-Provided Options using Clap

Modularize the Program

Here's our current "main.rs" file:

main.rs
/* main.rs */

mod images;
mod housekeeping;

use images::*;
use housekeeping::*;

fn main() {

    let opts: Options = get_options();
    
    let image_vecvec = string_to_vecvecstrings(opts.image_string);

    display_image(image_vecvec);
    
} // end of main()

fn get_options() -> Options {
    use clap::Parser; // Use c[ommand] l[ine] a[rgument] p[arser] to get command-line arguments.

    // Set up program-arguments possibilities.
    #[derive(Parser)]
      struct Arguments {

        /// Which image to animate? D[51] | C[51] | L[ittle] | B[oat] | T[win[engine]] | P[lane] | M[otor[cycle]] | J[ack] | F[erris]
        #[arg(long, short, default_value_t = String::from("D51"))]
        kind: String,

        /// Fly? If this switch is provided, the image will "fly".
        #[arg(short, long, default_value_t = false)]
        fly: bool,
              
        /// Ctrl-c?
        #[arg(short, long, default_value_t = false)]
        breakable: bool,
        
        /// Accident (only for some images)?
        #[arg(short, long, default_value_t = false)]
        accident: bool,
        
        /// No trailer (only for D51 & C51 trains)?
        #[arg(short, long, default_value_t = false)]
        no_trailer: bool,
    } // end of Arguments struct

    // The Options struct variable must be initialized; we're mostly using dummy values.
    let mut cli_opts = Options {
        kind: String::from("D51"),
        image_string: String::from(""),
        image_vecvec: vec![vec![String::from("")]],
        speed: 100,
        accident: false,
        fly: false,
        screen_height: 100,
        screen_width: 100,
        craft_height: 100,
        craft_length: 100,
    };
    
    // Get command-line arguments, if any.
    let args: Arguments = Arguments::parse();
    
    let image_string = match args.kind.to_uppercase().as_str() {
      "P" | "PLANE"                 =>  PLANE,
      "D" | "D51"                   =>  D51,
      "C" | "C51"                   =>  C51,
      "L" | "LITTLE"                =>  LITTLE,
      "B" | "BOAT"                  =>  BOAT,
      "T" | "TWIN" | "TWINENGINE"   =>  TWINENGINE,
      "M" | "MOTOR" | "MOTORCYCLE"  =>  MOTORCYCLE,        
      "J" | "JACK"                  =>  JACK,
      "F" | "FERRIS" | "MASCOT"     =>  FERRIS,
      _                             =>  D51,
    };
    cli_opts.image_string = image_string.to_string();
    return cli_opts; 
} // end of get_options()

fn string_to_vecvecstrings(image_string: String) -> Vec<Vec<String>> {

    //The first character in the original const, after the 'r"', is a newline; lose it.
    let image_string = &image_string[1..image_string.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];
            }
            // ... and then add the string to the current inner vector.
            outer_vec[inner_vec_num].push(each_line.to_string());
        } 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;
        }
    }
    // Return the finished vector of String vectors.
    outer_vec
} // end of string_to_vecvecstrings()


fn display_image(image_vecvec: Vec<Vec<String>>) {
    let mut frame_num = 0;
    for each_frame in image_vecvec {
      println!("Frame {}:", frame_num);
      for each_line in each_frame {
        println!("{}", each_line);
      }
    frame_num += 1;
    }
} // end of display_image()

And our current "housekeeping.rs" file:

housekeeping.rs
/* housekeeping.rs */

pub struct Options {
    pub kind: String,
    pub image_string: String,
    pub image_vecvec: Vec<Vec<String>>,
    pub speed: usize,
    pub accident: bool,
    pub fly: bool,
    pub screen_height: usize,
    pub screen_width: usize,
    pub craft_height: usize,
    pub craft_length: usize,
} // end of struct Options

The "images.rs" can be found back on the Put All Images In One File page.

Three Approaches

Everything but the "main()"-related Stuff in a Separate File

We could put all the extra stuff in the "main.rs" file into the "housekeeping.rs" file. This has the advantage of making the "main.rs" file pretty simple, while keeping the "extras" all in one place. It has the disadvantage that now "housekeeping.rs" contains most of the complexity. Let's look at how this would look.

main.rs
/* main.rs */

mod images;
mod housekeeping;

use images::*;
use housekeeping::*;

fn main() {

    let opts: Options = get_options();
    
    let image_vecvec = string_to_vecvecstrings(opts.image_string);

    display_image(image_vecvec);
    
} // end of main()


fn get_options() -> Options {
    use clap::Parser; // Use c[ommand] l[ine] a[rgument] p[arser] to get command-line arguments.

    // Set up program-arguments possibilities.
    #[derive(Parser)]
      struct Arguments {

        /// Which image to animate? D[51] | C[51] | L[ittle] | B[oat] | T[win[engine]] | P[lane] | M[otor[cycle]] | J[ack] | F[erris]
        #[arg(long, short, default_value_t = String::from("D51"))]
        kind: String,

        /// Fly? If this switch is provided, the image will "fly".
        #[arg(short, long, default_value_t = false)]
        fly: bool,
              
        /// Ctrl-c?
        #[arg(short, long, default_value_t = false)]
        breakable: bool,
        
        /// Accident (only for some images)?
        #[arg(short, long, default_value_t = false)]
        accident: bool,
        
        /// No trailer (only for D51 & C51 trains)?
        #[arg(short, long, default_value_t = false)]
        no_trailer: bool,
    } // end of Arguments struct

    // The Options struct variable must be initialized; we're mostly using dummy values.
    let mut cli_opts = Options {
        kind: String::from("D51"),
        image_string: String::from(""),
        image_vecvec: vec![vec![String::from("")]],
        speed: 100,
        accident: false,
        fly: false,
        screen_height: 100,
        screen_width: 100,
        craft_height: 100,
        craft_length: 100,
    };
    
    // Get command-line arguments, if any.
    let args: Arguments = Arguments::parse();
    
    let image_string = match args.kind.to_uppercase().as_str() {
      "P" | "PLANE"                 =>  PLANE,
      "D" | "D51"                   =>  D51,
      "C" | "C51"                   =>  C51,
      "L" | "LITTLE"                =>  LITTLE,
      "B" | "BOAT"                  =>  BOAT,
      "T" | "TWIN" | "TWINENGINE"   =>  TWINENGINE,
      "M" | "MOTOR" | "MOTORCYCLE"  =>  MOTORCYCLE,        
      "J" | "JACK"                  =>  JACK,
      "F" | "FERRIS" | "MASCOT"     =>  FERRIS,
      _                             =>  D51,
    };
    cli_opts.image_string = image_string.to_string();
    return cli_opts; 
} // end of get_options()

fn string_to_vecvecstrings(image_string: String) -> Vec<Vec<String>> {

    //The first character in the original const, after the 'r"', is a newline; lose it.
    let image_string = &image_string[1..image_string.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];
            }
            // ... and then add the string to the current inner vector.
            outer_vec[inner_vec_num].push(each_line.to_string());
        } 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;
        }
    }
    // Return the finished vector of String vectors.
    outer_vec
} // end of string_to_vecvecstrings()


fn display_image(image_vecvec: Vec<Vec<String>>) {
    let mut frame_num = 0;
    for each_frame in image_vecvec {
      println!("Frame {}:", frame_num);
      for each_line in each_frame {
        println!("{}", each_line);
      }
    frame_num += 1;
    }
} // end of display_image()

housekeeping.rs
/* housekeeping.rs */

use crate::images::*;

pub struct Options {
    pub kind: String,
    pub image_string: String,
    pub image_vecvec: Vec<Vec<String>>,
    pub speed: usize,
    pub accident: bool,
    pub fly: bool,
    pub screen_height: usize,
    pub screen_width: usize,
    pub craft_height: usize,
    pub craft_length: usize,
} // end of struct Options

pub fn get_options() -> Options {
    use clap::Parser; // Use c[ommand] l[ine] a[rgument] p[arser] to get command-line arguments.

    // Set up program-arguments possibilities.
    #[derive(Parser)]
      struct Arguments {

        /// Which image to animate? D[51] | C[51] | L[ittle] | B[oat] | T[win[engine]] | P[lane] | M[otor[cycle]] | J[ack] | F[erris]
        #[arg(long, short, default_value_t = String::from("D51"))]
        kind: String,

        /// Fly? If this switch is provided, the image will "fly".
        #[arg(short, long, default_value_t = false)]
        fly: bool,
              
        /// Ctrl-c?
        #[arg(short, long, default_value_t = false)]
        breakable: bool,
        
        /// Accident (only for some images)?
        #[arg(short, long, default_value_t = false)]
        accident: bool,
        
        /// No trailer (only for D51 & C51 trains)?
        #[arg(short, long, default_value_t = false)]
        no_trailer: bool,
    } // end of Arguments struct

    // The Options struct variable must be initialized; we're mostly using dummy values.
    let mut cli_opts = Options {
        kind: String::from("D51"),
        image_string: String::from(""),
        image_vecvec: vec![vec![String::from("")]],
        speed: 100,
        accident: false,
        fly: false,
        screen_height: 100,
        screen_width: 100,
        craft_height: 100,
        craft_length: 100,
    };
    
    // Get command-line arguments, if any.
    let args: Arguments = Arguments::parse();
    
    let image_string = match args.kind.to_uppercase().as_str() {
      "P" | "PLANE"                 =>  PLANE,
      "D" | "D51"                   =>  D51,
      "C" | "C51"                   =>  C51,
      "L" | "LITTLE"                =>  LITTLE,
      "B" | "BOAT"                  =>  BOAT,
      "T" | "TWIN" | "TWINENGINE"   =>  TWINENGINE,
      "M" | "MOTOR" | "MOTORCYCLE"  =>  MOTORCYCLE,        
      "J" | "JACK"                  =>  JACK,
      "F" | "FERRIS" | "MASCOT"     =>  FERRIS,
      _                             =>  D51,
    };
    cli_opts.image_string = image_string.to_string();
    return cli_opts; 
} // end of get_options()

pub fn string_to_vecvecstrings(image_string: String) -> Vec<Vec<String>> {

    //The first character in the original const, after the 'r"', is a newline; lose it.
    let image_string = &image_string[1..image_string.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];
            }
            // ... and then add the string to the current inner vector.
            outer_vec[inner_vec_num].push(each_line.to_string());
        } 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;
        }
    }
    // Return the finished vector of String vectors.
    outer_vec
} // end of string_to_vecvecstrings()

pub fn display_image(image_vecvec: Vec<Vec<String>>) {
    let mut frame_num = 0;
    for each_frame in image_vecvec {
      println!("Frame {}:", frame_num);
      for each_line in each_frame {
        println!("{}", each_line);
      }
    frame_num += 1;
    }
} // end of display_image()

Notice that moving the three functions into "housekeeping.rs" puts them into the private scope of that "housekeeping.rs" file. We need to make them publicly-scoped, so that the "main()" function in the "main.rs" file can see them. So we have added the keyword pub to the start of each of the three functions.

Also, the module "images[.rs]", being declared in the "main.rs" file, is known to the entire project (with the mod images; line), but the path to the images therein are not known to the entire project (crate), but only to each line that specifically mentions the path, or to the file which contains a "use" statement for the path. Since "main.rs" doesn't need to specify such paths, it no longer needs the use images::*; line. But "housekeeping.rs" does need that line. But since "housekeeping.rs" is not at the "top" ot the crate ("main.rs" is at the top, with "housekeeping.rs" being subordinate to "main.rs"), we can't just say use images;;*;; instead, we have to specify that the path starts at the top of the crate, with the line use crate::images::*;.

After making these changes, the program should still run, like so:

$ cargo run

or

$ cargo run -- --kind plane

Group Related Stuff in Their Own Files

Another option would be to put the housekeeping "stuff" in "housekeeping.rs", and the drawing "stuff" in a drawing.rs" file, and the getting-options "stuff" in a "get_opts.rs" file, etc. But it's hard to know where to draw a line, and that may add complexity of its own.

In our case, we could probably put the two functions, "get_options()" and "string_to_vecvecstrings()" together into one file. Currently we have a field in the "Options" struct, "image_vecvec", that is not even being used. Instead, in "main.rs", we are creating a discrete variable, "image_vecvec", which is bound to the same data that this unused field was designed for. If we have the first function, "get_options", call the second function, "string_to_vecvecstrings()", and put the data into the "Options" struct, the "main()" function will have access to that data without itself calling the second function.

This would simplify the "main.rs" file even further.

And since the only reason we have the "image_string" field in the "Options" struct is so that we can pass it back to "main() to be passed to "string_to_vecvecstrings()", by eliminating that middle step, we eliminate the need for that field, and thereby simplify the "Options" struct.

This is how that would look.

main.rs
/* main.rs */

mod get_opts;
mod housekeeping;
mod images;

use get_opts::*;
use housekeeping::*;

fn main() {

    let opts: Options = get_options();
    
    let image_vecvec = string_to_vecvecstrings(opts.image_string);

    display_image(opts.image_vecvec);
    
} // end of main()
housekeeping.rs
/* housekeeping.rs */

use crate::images::*;

pub struct Options {
    pub kind: String,
    pub image_string: String,
    pub image_vecvec: Vec<Vec<String>>,
    pub speed: usize,
    pub accident: bool,
    pub fly: bool,
    pub screen_height: usize,
    pub screen_width: usize,
    pub craft_height: usize,
    pub craft_length: usize,
} // end of struct Options

pub fn get_options() -> Options {
    use clap::Parser; // Use c[ommand] l[ine] a[rgument] p[arser] to get command-line arguments.

    // Set up program-arguments possibilities.
    #[derive(Parser)]
      struct Arguments {

        /// Which image to animate? D[51] | C[51] | L[ittle] | B[oat] | T[win[engine]] | P[lane] | M[otor[cycle]] | J[ack] | F[erris]
        #[arg(long, short, default_value_t = String::from("D51"))]
        kind: String,

        /// Fly? If this switch is provided, the image will "fly".
        #[arg(short, long, default_value_t = false)]
        fly: bool,
              
        /// Ctrl-c?
        #[arg(short, long, default_value_t = false)]
        breakable: bool,
        
        /// Accident (only for some images)?
        #[arg(short, long, default_value_t = false)]
        accident: bool,
        
        /// No trailer (only for D51 & C51 trains)?
        #[arg(short, long, default_value_t = false)]
        no_trailer: bool,
    } // end of Arguments struct

    // The Options struct variable must be initialized; we're mostly using dummy values.
    let mut cli_opts = Options {
        kind: String::from("D51"),
        image_string: String::from(""),
        image_vecvec: vec![vec![String::from("")]],
        speed: 100,
        accident: false,
        fly: false,
        screen_height: 100,
        screen_width: 100,
        craft_height: 100,
        craft_length: 100,
    };
    
    // Get command-line arguments, if any.
    let args: Arguments = Arguments::parse();
    
    let image_string = match args.kind.to_uppercase().as_str() {
      "P" | "PLANE"                 =>  PLANE,
      "D" | "D51"                   =>  D51,
      "C" | "C51"                   =>  C51,
      "L" | "LITTLE"                =>  LITTLE,
      "B" | "BOAT"                  =>  BOAT,
      "T" | "TWIN" | "TWINENGINE"   =>  TWINENGINE,
      "M" | "MOTOR" | "MOTORCYCLE"  =>  MOTORCYCLE,        
      "J" | "JACK"                  =>  JACK,
      "F" | "FERRIS" | "MASCOT"     =>  FERRIS,
      _                             =>  D51,
    };
    cli_opts.image_string = image_string.to_string();
    return cli_opts; 
} // end of get_options()

pub fn string_to_vecvecstrings(image_string: String) -> Vec<Vec<String>> {

    //The first character in the original const, after the 'r"', is a newline; lose it.
    let image_string = &image_string[1..image_string.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];
            }
            // ... and then add the string to the current inner vector.
            outer_vec[inner_vec_num].push(each_line.to_string());
        } 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;
        }
    }
    // Return the finished vector of String vectors.
    outer_vec
} // end of string_to_vecvecstrings()

pub fn display_image(image_vecvec: Vec<Vec<String>>) {
    let mut frame_num = 0;
    for each_frame in image_vecvec {
      println!("Frame {}:", frame_num);
      for each_line in each_frame {
        println!("{}", each_line);
      }
    frame_num += 1;
    }
} // end of display_image()
get_opts.rs
/* get_opts.rs */

use crate::housekeeping::*;
use crate::images::*;

pub fn get_options() -> Options {
    use clap::Parser; // Use c[ommand] l[ine] a[rgument] p[arser] to get command-line arguments.

    // Set up program-arguments possibilities.
    #[derive(Parser)]
      struct Arguments {

        /// Which image to animate? D[51] | C[51] | L[ittle] | B[oat] | T[win[engine]] | P[lane] | M[otor[cycle]] | J[ack] | F[erris]
        #[arg(long, short, default_value_t = String::from("D51"))]
        kind: String,

        /// Fly? If this switch is provided, the image will "fly".
        #[arg(short, long, default_value_t = false)]
        fly: bool,
              
        /// Ctrl-c?
        #[arg(short, long, default_value_t = false)]
        breakable: bool,
        
        /// Accident (only for some images)?
        #[arg(short, long, default_value_t = false)]
        accident: bool,
        
        /// No trailer (only for D51 & C51 trains)?
        #[arg(short, long, default_value_t = false)]
        no_trailer: bool,
    } // end of Arguments struct

    // The Options struct variable must be initialized; we're mostly using dummy values.
    let mut cli_opts = Options {
        kind: String::from("D51"),
        image_string: String::from(""),
        image_vecvec: vec![vec![String::from("")]],
        speed: 100,
        accident: false,
        fly: false,
        screen_height: 100,
        screen_width: 100,
        craft_height: 100,
        craft_length: 100,
    };
    
    // Get command-line arguments, if any.
    let args: Arguments = Arguments::parse();
    
    let image_string = match args.kind.to_uppercase().as_str() {
      "P" | "PLANE"                 =>  PLANE,
      "D" | "D51"                   =>  D51,
      "C" | "C51"                   =>  C51,
      "L" | "LITTLE"                =>  LITTLE,
      "B" | "BOAT"                  =>  BOAT,
      "T" | "TWIN" | "TWINENGINE"   =>  TWINENGINE,
      "M" | "MOTOR" | "MOTORCYCLE"  =>  MOTORCYCLE,        
      "J" | "JACK"                  =>  JACK,
      "F" | "FERRIS" | "MASCOT"     =>  FERRIS,
      _                             =>  D51,
    };
    cli_opts.image_string = image_string.to_string();
    cli_opts.image_vecvec = string_to_vecvecstrings(image_string.to_string());

    return cli_opts; 
} // end of get_options()

pub fn string_to_vecvecstrings(image_string: String) -> Vec<Vec<String>> {

    //The first character in the original const, after the 'r"', is a newline; lose it.
    let image_string = &image_string[1..image_string.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];
            }
            // ... and then add the string to the current inner vector.
            outer_vec[inner_vec_num].push(each_line.to_string());
        } 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;
        }
    }
    // Return the finished vector of String vectors.
    outer_vec
} // end of string_to_vecvecstrings()

Go ahead and test these changes:

# cargo run -- --kind plane

Then we can put "display_image()" into its own file, like so:

display.rs
/* display.rs */

pub fn display_image(image_vecvec: Vec<Vec<String>>) {
    let mut frame_num = 0;
    for each_frame in image_vecvec {
      println!("Frame {}:", frame_num);
      for each_line in each_frame {
        println!("{}", each_line);
      }
    frame_num += 1;
    }
} // end of display_image()
housekeeping.rs
/* housekeeping.rs */

pub struct Options {
    pub kind: String,
    pub image_vecvec: Vec<Vec<String>>,
    pub speed: usize,
    pub accident: bool,
    pub fly: bool,
    pub screen_height: usize,
    pub screen_width: usize,
    pub craft_height: usize,
    pub craft_length: usize,
} // end of struct Options


pub fn display_image(image_vecvec: Vec<Vec<String>>) {
    let mut frame_num = 0;
    for each_frame in image_vecvec {
      println!("Frame {}:", frame_num);
      for each_line in each_frame {
        println!("{}", each_line);
      }
    frame_num += 1;
    }
} // end of display_image()
main.rs
/* main.rs */

mod display;
mod get_opts;
mod housekeeping;
mod images;

use display::*;
use get_opts::*;
use housekeeping::*;

fn main() {
    let opts: Options = get_options();

    display_image(opts.image_vecvec);
} // end of main()

Another test here should work.

# cargo run -- --kind plane

Everything in its Own File

A third option might be to put each function in its own ".rs" file. At this point, that just means breaking "get_opts.rs" into two separate files, like so:

get_opts.rs
/* get_opts.rs */

use crate::housekeeping::*;
use crate::images::*;

pub fn get_options() -> Options {
    use clap::Parser; // Use c[ommand] l[ine] a[rgument] p[arser] to get command-line arguments.

    // Set up program-arguments possibilities.
    #[derive(Parser)]
      struct Arguments {

        /// Which image to animate? D[51] | C[51] | L[ittle] | B[oat] | T[win[engine]] | P[lane] | M[otor[cycle]] | J[ack] | F[erris]
        #[arg(long, short, default_value_t = String::from("D51"))]
        kind: String,

        /// Fly? If this switch is provided, the image will "fly".
        #[arg(short, long, default_value_t = false)]
        fly: bool,
              
        /// Ctrl-c?
        #[arg(short, long, default_value_t = false)]
        breakable: bool,
        
        /// Accident (only for some images)?
        #[arg(short, long, default_value_t = false)]
        accident: bool,
        
        /// No trailer (only for D51 & C51 trains)?
        #[arg(short, long, default_value_t = false)]
        no_trailer: bool,
    } // end of Arguments struct

    // The Options struct variable must be initialized; we're mostly using dummy values.
    let mut cli_opts = Options {
        kind: String::from("D51"),
        image_vecvec: vec![vec![String::from("")]],
        speed: 100,
        accident: false,
        fly: false,
        screen_height: 100,
        screen_width: 100,
        craft_height: 100,
        craft_length: 100,
    };
    
    // Get command-line arguments, if any.
    let args: Arguments = Arguments::parse();
    
    let image_string = match args.kind.to_uppercase().as_str() {
      "P" | "PLANE"                 =>  PLANE,
      "D" | "D51"                   =>  D51,
      "C" | "C51"                   =>  C51,
      "L" | "LITTLE"                =>  LITTLE,
      "B" | "BOAT"                  =>  BOAT,
      "T" | "TWIN" | "TWINENGINE"   =>  TWINENGINE,
      "M" | "MOTOR" | "MOTORCYCLE"  =>  MOTORCYCLE,        
      "J" | "JACK"                  =>  JACK,
      "F" | "FERRIS" | "MASCOT"     =>  FERRIS,
      _                             =>  D51,
    };
    cli_opts.image_vecvec = string_to_vecvecstrings(image_string.to_string());

    return cli_opts; 
} // end of get_options()
convert_to_vec.rs
/* convert_to_vec.rs */

pub fn string_to_vecvecstrings(image_string: String) -> Vec<Vec<String>> {

    //The first character in the original const, after the 'r"', is a newline; lose it.
    let image_string = &image_string[1..image_string.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];
            }
            // ... and then add the string to the current inner vector.
            outer_vec[inner_vec_num].push(each_line.to_string());
        } 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;
        }
    }
    // Return the finished vector of String vectors.
    outer_vec
} // end of string_to_vecvecstrings()
main.rs
/* main.rs */

mod convert_to_vec;
mod display;
mod get_opts;
mod housekeeping;
mod images;
...
get_opts.rs
/* get_opts.rs */

use crate::convert_to_vec::*;
use crate::housekeeping::*;
use crate::images::*;
...

Do One Thing; Do that One Thing Well

A core tenet of Unix/Linux philosophy is that each part should do one thing, and it should do that one thing well. Currently, we have six different ".rs" files, each one doing one thing. (Is it doing that one thing well? Maybe.) It doesn't always make sense to put each function into its own file. Sometimes it makes more sense to be more integrative, such as putting several related functions into a common library file, or several related library files into a common library crate, etc. But for now, we'll work with these six files. Just in case it's needed, here's a Listing of the Code After Modularizing the Project.