Convert "sl" Source Code from C to Rust

Complete code listing to this point.

Move Images Back to main()

In the last tutorial, Add a Jumping Jack, we realized that having separate text files for the images might be problematic, and we decided to instead include the images within the main program. That's what we'll be doing here.

We could duplicate the "src/d51.rs"/"get_d51()" function for each of the image kinds, but I think we can make it a little easier to draw new images by combining some features of the "get_d51()" function and some features of the "get_image()" function. Let's try it with the Jumping Jack image.

Prep the "jack.rs" File

If you have not yet deleted "src/jack.rs", do so now, and then recreate it (or just delete all the contents out of the existing file).

Then copy the following and paste it into "src/jack.rs".

src/jack.rs
/* jack.rs */

pub const JACK: &str = r"
' \ 0 /   '
'  \|/    '
'   |     '
'  / \    '
'_/   \_  '
'         '

'         '
' __0__   '
'/  |  \  '
'  / \    '
' _\ /_   '
'         '

'         '
'   o     '
' /\ /\   '
' |/ \|   '
' _\ /_   '
'         '

'         '
' __0__   '
'/  |  \  '
'  / \    '
' _\ /_   '
'         '
"; // end of JACK

The only differences are that we changed the double-quotes into single-quotes, and then wrapped all of those in one pair of single-quotes, which is on the right-hand side of an assignment statement to a "raw" string (the "r"), assigning the data to a public constant named "JACK" (constants in Rust should be all-caps), and then we finished out that assignment statement with a semi-colon, and added a couple of comments. Very little difference to what the file was before.

So rather than reading this data from a file into a variable, we'll "read" it from this constant, which is sort of like an immutable variable, but a tad less "expensive" in terms of computing resources. In fact, we could use a variable instead if we preferred, but that's slightly more complex to set up in our "src/jack.rs" file, and we'd like to keep this file as simple as possible. The artist who creates an image file will need to use single-quotes on each line rather than double-quotes, and then wrap the whole set in double-quotes to turn it into a single string, like the single string that would be read from a file. In addition, to allow the string to contain single backslashes, we need to set it as a "raw" string; that's the r just before the first double-quote mark.

Refactor the Code So Only One Match Is Required

Right now, if someone creates a new image, say, a rocket-ship image, etc, that programmer will have to edit the main program in two different places: the "match" section in "extras.rs"/"draw_image()", and the "match" section in "extras"/"parse_opts". To reduce the chance of human error, it would be better if there was only one "match" section that needs to be edited when a new image is added to the program. There are a couple of ways we could accomplish this, but perhaps the best way is to choose, in the "parse_options" function, which image we'll be drawing, and to return the matching image to the calling program ("main()"). After all, which image we draw is an option.

Refactor "parse_opts()".

Here are the changes we need to make to the "extras.rs"/parse_opts()" function, commenting out much of the "match" section for now, just to make sure this new method works:

extras.rs / parse_opts
pub fn parse_opts() -> (u64, bool, String, bool, &'static str) { // The constant has been copied into a &str value, which will disappear when
                                                                 // this function ends, preventing it from getting returned to the calling routine. So we
                                                                 // specify it as a 'static &str, which keeps it in existence for the lifetime of
                                                                 // the program.

...

    // Set "kind".
    let (kind, image) = match (&switches.kind.to_uppercase()).as_str() {  // Instead of the match returning one value, it returns two as a tuple.
//        "D" | "D51" => String::from("D51"), // Alternative way of doing '"D51".to_string()'.
//        "C" | "C51" => String::from("C51"),
//        "L" | "LITTLE" => String::from("LITTLE"),
        "J" | "JACK" => ( String::from("JACK"), crate::jack::JACK ), // Returning the String "JACK", and the image in the constant named "JACK"; returning both as a tuple.
//        "B" | "BOAT" => String::from("BOAT"),
//        "P" | "PLANE" => String::from("PLANE"),
        _ => {
            // If none of them match, then ...
            println!("Invalid input; run program with '--help' option for more information. You entered '{}', so the default of 'jack''d51' was used.", switches.kind);
//            String::from("D51") // While testing this setup, D51 is broken, so use Jack instead for now.
            ( String::from("JACK"), crate::jack::JACK )
        }
    };

    return (speed, switches.fly, kind, switches.oops, image);
} // end of parse_opts()

The String::from("JACK"), crate::jack::JACK ) lines could be reduced to String::from("JACK"), crate::jack::JACK ) if we added a "use" statement at the top of the "extras.rs" file, like this: use crate::jack::JACK (or use crate::jack::*;, if you wanted to include all the [potential] code within the "jack.rs" file). crate is roughly equivalent to "the top level of this project we're working on", so it kind of corresponds to "~/projects/RUST/sl/src". Kind of. Not exactly. But close enough for now.

And then in "main()", we'll temporarily comment out the call to "draw_image", and just do a rough drawing from "main() as a test of these changes. We also need to make the program aware of the new "jack" module (roughly, "jack.rs").

main.rs / main()
...
  mod jack;
...
  fn main() {
    let (speed, fly, kind, oops, image) = parse_opts(); // Get the program options from the command-line, if any.

  println!("{}", image);
//    draw_image(speed, fly, kind, oops);
} // end of main()

Test the Mods to "parse_opts()".

This should print out the three frames of Jack, in single-quotes, with a lot of warnings.

$ cargo run -- --kind=jack
...
warning: `sl` (bin "sl") generated 10 warnings
    Finished dev [unoptimized + debuginfo] target(s) in 0.68s
     Running `target/debug/sl -s=f -k=j`

' \ 0 /   '
'  \|/    '
'   |     '
'  / \    '
'_/   \_  '
'         '

'         '
' __0__   '
'/  |  \  '
'  / \    '
' _\ /_   '
'         '

'         '
'   o     '
' /\ /\   '
' |/ \|   '
' _\ /_   '
'         '
$

Refactor "draw_image()".

Now that the "main()" function has the "image" variable, we'll need to pass that on to the "draw_image()" function. So make the following changes to "main()":

  fn main() {
    let (speed, fly, kind, oops, image) = parse_opts(); // Get the program options from the command-line, if any.

  println!("{}", image);
//    draw_image(speed, fly, kind, oops, image);
} // end of main()

We need to make some pretty substantial modifications to both "draw_image()" and "get_image()". The "get_image()" function had been reading the image from a text file, into a &str, and then converting that &str to a vector of vectors of Strings. Now we already have the image in a &str, and just need to convert it to a vector of vector of Strings. So let's rename "get_image()" to "ampstr_to_vecvecstring()", and then modify it as needed:

"get_image()" to "ampstr_to_vecvecstring()"
pub fn ampstr_to_vecvecstring(image_str: &str) -> Vec<Vec<String>> {
    /*  This function recieves a series of ASCII-art image "frames" in the variable "image_str", as one long
        string of UTF-8 characters (think of 'em as ASCII chars, though). This string is then broken down into
        separate lines, breaking at the newline/end-of-line characters, and each line is converted from a &str
        into a String, and then pushed onto a vector of Strings. When a blank line is found, signifying the end
        of one frame and the beginning of another, we skip on to the next loop, and create a new vector at the
        same time, and then repeat the above process, pushing the next frame into this new vector. Eventually,
        each of the image frames is put into its own vector, and all these vectors are put into an outer vector.
    */

    let mut outer_vec: Vec<Vec<String>> = Vec::new();               // Outer vec holding all the inner frame vecs.
    outer_vec.push(Vec::new());                                     // Create first inner_image_frame vec, and push it into the outer_vec.

    let mut inner_image_frame: usize = 0;                                 // To keep track of which image frame vector we're working with. First = 0.
    
    // Process each line of the image_str.
    for each_line in image_str.lines() {                            // Break the &str into lines at the newline.
        if each_line != "" {                                        // If the line is not blank...
            each_line.replace("'", " ");                            // ... replace the single-quotes with spaces, and ...
            let line_string = each_line.to_string();                // ... convert the &str variable to a String variable.
            outer_vec[inner_image_frame].push(line_string);         // Then push that String into the current inner inner_image_frame vec.
        } else {                                                    // Else, if the line is blank, ignore it, and ...
            inner_image_frame += 1;                                 // ... count a new inner inner_image_frame vec, and then ...
            outer_vec.push(Vec::new());                             // ... create that new inner inner_image_frame vec.
        }
    }
    return outer_vec;
} // end of ampstr_to_vecvecstring()

We could call the above function to convert the &str to a vector of vector of Strings from within the "draw_image()" function, or we could call it before the draw_image() function, from within "main()". It just depends on if we want the "draw_image()" function to take as its input a more plain, more raw text string of the ASCII-art, and process it as needed, or if we want the "draw_image() function to take as its input an already-processed ASCII-art image. The first method is more "integrated"; the second is more UNIX-y, philosophically, in that each piece is small, doing one job and doing it well, and then connecting the pieces together. I think I'll opt for the UNIX-y method; I think it'll be easier to troubleshoot and follow the logic, etc.

So, in "main()", we'll get a &str back from the "parse_opts()" function; then we'll send that off to be converted to a vector of vector of Strings, and then we'll send that vector of vector of Strings to the "draw_image()" function.

"main.rs"/"main()"
fn main() {
    let (speed, fly, kind, oops, image_str) = parse_opts();
    let image_vec = ampstr_to_vecvecstr(image_str);
    // "draw_image(speed, fly, kind, oops, image_vec)
} // end of main()

Notice we've left the "draw_image()" function commented out. This is so we can simplify things and do some testing at this point:

"main.rs"/"main()"
fn main() {
    let (speed, fly, kind, oops, image_str) = parse_opts();
println!("{:?}", image_str);
    println!("Above is the amperstring");
    let image_vec = ampstr_to_vecvecstring(image_str);
    println!("Below is the converted vecvecstring");
println!("{:?}", image_vec);
    // draw_image(speed, fly, kind, oops, image_vec)
} // end of main()

If you run this, you should get output like this (with quite a few warnings):

    Finished dev [unoptimized + debuginfo] target(s) in 0.65s
     Running `target/debug/sl --kind=jack`
"\n' \\ 0 /   '\n'  \\|/    '\n'   |     '\n'  / \\    '\n'_/   \\_  '\n'         '\n\n'         '\n' __0__   '\n'/  |  \\  '\n'  / \\    '\n' _\\ /_   '\n'         '\n\n'         '\n'   o     '\n' /\\ /\\   '\n' |/ \\|   '\n' _\\ /_   '\n'         '\n"
Above is the amperstring
Below is the converted vecvecstring
[[], ["  \\ 0 /    ", "   \\|/     ", "    |      ", "   / \\     ", " _/   \\_   ", "           "], ["           ", "  __0__    ", " /  |  \\   ", "   / \\     ", "  _\\ /_    ", "           "], ["           ", "    o      ", "  /\\ /\\    ", "  |/ \\|    ", "  _\\ /_    ", "           "]]

Good! This means our convertor works.

Mostly. I see a problem.

You see that [[], [... at the beginning of the output after the "Below is the converted vecvecstring" line? That's an empty vector. It's being generated by the \n... newline in the beginning of the output above the "Above is the amperstring" line.

We don't want that.

That's being generated in our "jack.rs" file, in our "JACK" constant. Here are the first few lines of that constant:

pub const JACK: &str = r"
' \ 0 /   '
'  \|/    '

The &str begins right after the r", and immediately after that is a newline character (\n) (which we don't see), followed by what we want to be the first line in the constant, ' \ 0 / ', followed by another newline character, followed by the next line, ' \|/ ', followed by another newline character, and so on and so on.

We need to somehow get rid of that first unwanted newline, which is ultimately generating an empty vector. We could rewrite the constant, like so:

pub const JACK: &str = r"' \ 0 /   '
'  \|/    '

..., but that's ugly, and puts more of a burden on the artist of the ASCII-art. Other modifications to the constant have similar drawbacks. If we assume that all our image constants are built consistently, in this same way, we can handle the issue in our programming. Let's go that route, and leave the "JACK" constant as we already have it.

We could fix it in the "parse_opts()" function, just before the &str gets returned to be used by the rest of the program. However, it's probably best to let the parse_opts()" function return the constant exactly as it has been written, and let some other part of the program clean it up. Snce the "ampstr_to_vecvecstring()" function is already a custom-designed convertor, it probably makes the most sense to make the clean-up there. Here's that function after necessary (and perhaps unnecessary) changes have been made, including variable-name changes, to perhaps be more self-explanatory:

ampstr_to_vecvecstring()
pub fn ampstr_to_vecvecstring(image_str: &str) -> Vec<Vec<String>> {
    /*  This function recieves a series of ASCII-art image "inner_vecs" in the variable "image_str", as one long
        string of UTF-8 characters (think of 'em as ASCII chars, though). This string is then broken down into
        separate lines, breaking at the newline/end-of-line characters, and each line is converted from a &str
        into a String, and then pushed onto a vector of Strings. When a blank line is found, signifying the end
        of one inner_vec and the beginning of another, we skip on to the next loop, and create a new vector at the
        same time, and then repeat the above process, pushing the next inner_vec into this new vector. Eventually,
        each of the image inner_vecs is put into its own vector, and all these vectors are put into an outer vector.
    */

    // The const for the ASCII-art image needs to be cleaned up; for that, we need a mutable String.
    let mut image_string = image_str.to_owned();

    // The const for the ASCII-art image has unwanted single-quotes; let's remove those.
    image_string = image_string.replace("'", "");

    // The const for the ASCII-art image has an unwanted newline at the top of the string; let's remove that.
//    image_string.remove(0);       // This seems to cause problems; I'm not sure what's going on, and am too lazy to investigate.

    // And finally, let's put a trailing space on each line;
    image_string.replace("\n", " \n");

    let mut outer_vec: Vec<Vec<String>> = Vec::new(); // Outer vec holding all the inner inner_vec vecs.
    outer_vec.push(Vec::new()); // Create first inner_vec, and push it into the outer_vec.

    let mut inner_vec: usize = 0; // To keep track of which image inner_vec vector we're working with. First = 0.

    // Process each line of the image_string.
    for each_line in image_string.lines() {
        // Break the string into lines at the newline.
        if each_line != "" {
            // If the line is not blank...
            outer_vec[inner_vec].push(line_string); // ... then convert that line to a String and push it into the current inner_vec.
        } else {
            // Else, if the line is blank, ignore it, and ...
            nner_vec += 1; // ... count a new inner inner_vec, and then ...
            outer_vec.push(Vec::new()); // ... create that new inner_vec.
        }
    }
    return outer_vec;
} // end of ampstr_to_vecvecstring()

Now if you try running your test, you should get:

$ cargo run -- --kind=jack
...
warning: `sl` (bin "sl") generated 12 warnings
    Finished dev [unoptimized + debuginfo] target(s) in 0.67s
     Running `target/debug/sl --kind=jack`
"' \\ 0 /   '\n'  \\|/    '\n'   |     '\n'  / \\    '\n'_/   \\_  '\n'         '\n\n'         '\n' __0__   '\n'/  |  \\  '\n'  / \\    '\n' _\\ /_   '\n'         '\n\n'         '\n'   o     '\n' /\\ /\\   '\n' |/ \\|   '\n' _\\ /_   '\n'         '\n"
Above is the amperstring
Below is the converted vecvecstring
[["' \\ 0 /   '", "'  \\|/    '", "'   |     '", "'  / \\    '", "'_/   \\_  '", "'         '"], ["'         '", "' __0__   '", "'/  |  \\  '", "'  / \\    '", "' _\\ /_   '", "'         '"], ["'         '", "'   o     '", "' /\\ /\\   '", "' |/ \\|   '", "' _\\ /_   '", "'         '"]]

Yay! No more unwanted newline or empty vector!

Don't let the double-backslashes throw you; they're only used internally in the innards of Rust, and the "debug formatter" (the "{:?}") doesn't know to dispay them as a single backslash. But the default println! formatter does. You can see this if we print the variable a little better in our test:

"main.rs"/"main()"
fn main() {
    let (speed, fly, kind, oops, image_str) = parse_opts();
println!("{:?}", image_str);
    println!("Above is the amperstring");
    let image_vec = ampstr_to_vecvecstring(image_str);
    println!("Below is the converted vecvecstring");
println!("{:?"}, image_vec);
for every_frame in &image_vec {
  for every_line in every_frame {
    println!("{}", every_line);
  }
}
    // draw_image(speed, fly, kind, oops, image_vec)
} // end of main()

That should generate:

warning: `sl` (bin "sl") generated 12 warnings
    Finished dev [unoptimized + debuginfo] target(s) in 0.69s
     Running `target/debug/sl --kind=jack`
 \ 0 /   
  \|/    
   |     
  / \    
_/   \_  
         
         
 __0__   
/  |  \  
  / \    
 _\ /_   
         
         
   o     
 /\ /\   
 |/ \|   
 _\ /_   
         
         

Now let's get our "draw_image()" function to work. Notice how we have gotten rid of the entire second "match" section from our program. Notice that I've changed some variable names to make them more self-explanatory.

draw_image()
pub fn draw_image(delay: u64, fly: bool, kind: String, oops: bool, image_vec: Vec<Vec<String>>) {
    initscr(); // Start ncurses, initializing "stdscr()".
    noecho(); // Don't echo keypresses to screen.
    curs_set(CURSOR_VISIBILITY::CURSOR_INVISIBLE); // Turn off the display of the cursor.

    // Get the screen dimensions.
    let (mut screen_height, mut screen_width) = (0, 0);
    getmaxyx(stdscr(), &mut screen_height, &mut screen_width); // Dims are now in screen_height/width.

    // Load ASCII-art into "image" variable (a vector of vectors holding strings).
    let mut image: Vec<Vec<String>> = vec![vec!["".to_string()]];
    image = match kind.as_str() {
        "D51" => get_d51(),
        // "C51" => get_image("src/c51.txt"),
        // "LITTLE" => get_image("src/little.txt"),
        //   	"JACK" => get_image(jack::jack),
        // "BOAT" => get_boat(),
        // "PLANE" => get_plane(),
        &_ => {
            println!(
                "Invalid kind; you entered {}. The default of 'D51' was used instead.",
                kind
            );
            get_d51()
        }
    };

    let frames = image.len() as i32; // How many animation cel frames in this image?
    let count_of_inner_vecs = image.len() as i32; // How many animation cel inner_vecs in this image?
    let length = image[0][0].len() as i32; // How long (width in screen columns/chars) is the train?
    let height = image[0].len() as i32; // How tall is the train?
    let mut row = screen_height - height; // We'll draw the train at the bottom of the screen.
    let ms = Duration::from_millis(delay); // We'll be pausing this many milliseconds between frames.

    let mut inner_vec = count_of_inner_vecs - 1; // Let's start with the highest-numbered inner_vec, & count down. This will fix the "reverse" issue.
    for col in ((-length)..screen_width).rev() {
        //Count down from right-edge until nose of train drags all of train off left-edge.
        let mut line = row; // "line" of ASCII-art to display; "row" is where.
        let current_inner_vec = &image_vec[inner_vec as usize]; // Strings are referenced by a usize, so here's another conversion.
        for each_line in current_inner_vec {
            my_mvaddstr(line, col, &each_line, screen_width, screen_height); // Display each line of the inner_vec.
            line = line + 1; // Move to next row for next line of ASCII-art.
        }
        refresh(); // Display the image.
        thread::sleep(ms); // Pause to let our eyes register the image.
        inner_vec = inner_vec - 1; // Prepare to display next inner_vec.
        if inner_vec < 0 {
            // If we've gotten down to last inner_vec...
            inner_vec = count_of_inner_vecs - 1; // ... start over.
            if fly {
                // And check to see if we're flying. If so...
                line = -1 + row + height; // ... erase last (bottom) line drwn with enough repeated spaces fo train's length.
                my_mvaddstr(
                    line,
                    col,
                    &" ".repeat(length as usize),
                    screen_width,
                    screen_height,
                ); // No need to refresh(); next inner_vec will do it for us.
                row -= 1; // ... raise the next drawing one row up.
            }
        }
    }

    /* Once finished, terminate ncurses. */
    endwin();
} // end of draw_image()

And then we restore our "main()" back to normal"

main()
fn main() {
    let (speed, fly, kind, oops, image_str) = parse_opts();
    let image_vec = ampstr_to_vecvecstring(image_str);
for every_frame in &image_vec {
  for every_line in every_frame {
    println!("{}", every_line);
  }
}
    // draw_image(speed, fly, kind, oops, image_vec)
} // end of main()

And that should result in a working Jumping Jack.

I notice though, in Fly mode, Jack flies off the top of my terminal window long before he gets to the edge, so I think I'll configure the images to start over at the bottom if they fly completely off the top. In "draw_image()":

...
 if inner_vec < 0 {
            // If we've gotten down to last inner_vec...
            inner_vec = count_of_inner_vecs - 1; // ... start over.
            if fly {
                // And check to see if we're flying. If so...
                line = -1 + row + height; // ... erase last (bottom) line drwn with enough repeated spaces fo train's length.
                my_mvaddstr(
                    line,
                    col,
                    &" ".repeat(length as usize),
                    screen_width,
                    screen_height,
                ); // No need to refresh(); next inner_vec will do it for us.
                row -= 1; // ... raise the next drawing one row up.
                if row < 0 - height {
                    row = screen_height;
                }
            }
        }
...