Convert "sl" Source Code from C to Rust

Add the C51 Train

Click Here if you would like to see a complete code listing to this point.

In the last tutorial, Get More User-provided Options, we added the options to select different images. Here, we'll be adding the C51 model of train. At this point, we could just duplicate the "d51.rs" file and rename it to "c51.rs", and rename all the "d51" bits to "c51", and make some minor modifications elsewhere, and we'd be done with it. But as I think about that, I realize that's going to duplicate a lot of code for each different image we want to use, and I wonder if there might be a better way to do this.

I also think about how the author of a new image, of a boat, or a plane, or a rocket, etc, has to fit his/her ASCII-art into the confines of a vector of vector of Strings, and has to deal with other complexities, and wonder if that can be simplified.

I have already gone quite a ways with this tutorial using my first Plan B for the C51 train (and other images), which is the path you'll follow if you continue following this page, but I'm now thinking I'll chase after a second Plan B (ooh, I know, we can call it "Plan C"!) for the C51 train (and other images). You're free to follow either path (be aware that Plan B eventually stops before total completion, but has a lot of informative Rust programming involved), but I think you'll find Plan C to be simpler, and perhaps, better. Continue reading on this page for the original Plan B, or Click Here for Plan C.

Create a C51 Module

The first thing to do is to create a new "src/c51.rs" file, containing the following:

c51.rs
/* c51.rs */

pub fn draw_c51() {
  println!("This is the 'c51' module.");
} // end of draw_c51()

Then edit "src/main()" to look like this:

main.rs
mod extras;
mod d51;
mod c51;

use extras::*;
use d51::*;
use c51::*;

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

    draw_d51(speed, fly, oops);
    match kind.as_str() {                       
      "D51" => draw_d51(speed, fly, oops),
      "C51" => draw_c51(),
      &_ => println!("nope"),
    }
} // end of main()

When you compile/run this program, it should function as it did previously, with the exception that if you tell it to run the "C51" train (e.g., $ cargo run -- --kind c51), you'll just get a message that says, "This is the 'c51' module."

$ cargo run -- --kind c51
   Compiling sl v0.1.0 (/home/kent/projects/RUST/sl)
warning: unused variable: `oops`
  --> src/d51.rs:31:40
   |
31 | pub fn draw_d51(delay: u64, fly: bool, oops: bool) {
   |                                        ^^^^ help: if this is intentional, prefix it with an underscore: `_oops`
   |
   = note: `#[warn(unused_variables)]` on by default

warning: `sl` (bin "sl") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.73s
     Running `target/debug/sl --kind c51`
This is the 'c51' module.
$

Add Other Modules

While we're here, we could add similar modules for "little", "jack", "boat", and "plane", but that might be getting ahead of ourselves, especially if we wind up discovering we don't like the method we're using for the C51.

Get the C51 Image

Find where you have the C-version of "sl", and copy the "sl.h" file to "src/c51.txt" (or you can just copy the text below). Then edit "src/c51.txt" and remove everything that is not the drawings in quotes, and put they main body of the train over each set of wheels, and do whatever other massaging of the date that needs to be done to produce the following:

c51.txt
"        ___                                            "
"       _|_|_  _     __       __             ___________"
"    D__/   \\_(_)___|  |__H__|  |_____I_Ii_()|_________|"
"     | `---'   |:: `--'  H  `--'         |  |___ ___|  "
"    +|~~~~~~~~++::~~~~~~~H~~+=====+~~~~~~|~~||_| |_||  "
"    ||        | ::       H  +=====+      |  |::  ...|  "
"|    | _______|_::-----------------[][]-----|       |  "
"| /~~ ||   |-----/~~~~\\  /[I_____I][][] --|||_______|__"
"------'|oOo|==[]=-     ||      ||      |  ||=======_|__"
"/~\\____|___|/~\\_|   O=======O=======O  |__|+-/~\\_|     "
"\\_/         \\_/  \\____/  \\____/  \\____/      \\_/       "

"        ___                                            "
"       _|_|_  _     __       __             ___________"
"    D__/   \\_(_)___|  |__H__|  |_____I_Ii_()|_________|"
"     | `---'   |:: `--'  H  `--'         |  |___ ___|  "
"    +|~~~~~~~~++::~~~~~~~H~~+=====+~~~~~~|~~||_| |_||  "
"    ||        | ::       H  +=====+      |  |::  ...|  "
"|    | _______|_::-----------------[][]-----|       |  "
"| /~~ ||   |-----/~~~~\\  /[I_____I][][] --|||_______|__"
"------'|oOo|===[]=-    ||      ||      |  ||=======_|__"
"/~\\____|___|/~\\_|    O=======O=======O |__|+-/~\\_|     "
"\\_/         \\_/  \\____/  \\____/  \\____/      \\_/       "

"        ___                                            "
"       _|_|_  _     __       __             ___________"
"    D__/   \\_(_)___|  |__H__|  |_____I_Ii_()|_________|"
"     | `---'   |:: `--'  H  `--'         |  |___ ___|  "
"    +|~~~~~~~~++::~~~~~~~H~~+=====+~~~~~~|~~||_| |_||  "
"    ||        | ::       H  +=====+      |  |::  ...|  "
"|    | _______|_::-----------------[][]-----|       |  "
"| /~~ ||   |-----/~~~~\\  /[I_____I][][] --|||_______|__"
"------'|oOo|===[]=- O=======O=======O  |  ||=======_|__"
"/~\\____|___|/~\\_|      ||      ||      |__|+-/~\\_|     "
"\\_/         \\_/  \\____/  \\____/  \\____/      \\_/       "

"        ___                                            "
"       _|_|_  _     __       __             ___________"
"    D__/   \\_(_)___|  |__H__|  |_____I_Ii_()|_________|"
"     | `---'   |:: `--'  H  `--'         |  |___ ___|  "
"    +|~~~~~~~~++::~~~~~~~H~~+=====+~~~~~~|~~||_| |_||  "
"    ||        | ::       H  +=====+      |  |::  ...|  "
"|    | _______|_::-----------------[][]-----|       |  "
"| /~~ ||   |-----/~~~~\\  /[I_____I][][] --|||_______|__"
"------'|oOo|==[]=- O=======O=======O   |  ||=======_|__"
"/~\\____|___|/~\\_|      ||      ||      |__|+-/~\\_|     "
"\\_/         \\_/  \\____/  \\____/  \\____/      \\_/       "

"        ___                                            "
"       _|_|_  _     __       __             ___________"
"    D__/   \\_(_)___|  |__H__|  |_____I_Ii_()|_________|"
"     | `---'   |:: `--'  H  `--'         |  |___ ___|  "
"    +|~~~~~~~~++::~~~~~~~H~~+=====+~~~~~~|~~||_| |_||  "
"    ||        | ::       H  +=====+      |  |::  ...|  "
"|    | _______|_::-----------------[][]-----|       |  "
"| /~~ ||   |-----/~~~~\\  /[I_____I][][] --|||_______|__"
"------'|oOo|=[]=- O=======O=======O    |  ||=======_|__"
"/~\\____|___|/~\\_|      ||      ||      |__|+-/~\\_|     "
"\\_/         \\_/  \\____/  \\____/  \\____/      \\_/       "

"        ___                                            "
"       _|_|_  _     __       __             ___________"
"    D__/   \\_(_)___|  |__H__|  |_____I_Ii_()|_________|"
"     | `---'   |:: `--'  H  `--'         |  |___ ___|  "
"    +|~~~~~~~~++::~~~~~~~H~~+=====+~~~~~~|~~||_| |_||  "
"    ||        | ::       H  +=====+      |  |::  ...|  "
"|    | _______|_::-----------------[][]-----|       |  "
"| /~~ ||   |-----/~~~~\\  /[I_____I][][] --|||_______|__"
"------'|oOo|=[]=-      ||      ||      |  ||=======_|__"
"/~\\____|___|/~\\_|  O=======O=======O   |__|+-/~\\_|     "
"\\_/         \\_/  \\____/  \\____/  \\____/      \\_/       "

We're going to have our program read this file, and build our vector of vectors of Strings from it. This format, without all the vector-code overhead, will make it easier for ASCII-artists to produce their ASCII-art.

Make the D51 Drawing Routines more Generic, To Be Used With All the Images

As we think about this, most everything in the "src/d51.rs" file will need to be duplicated for the C51 train, and for every other image we might want to draw. Rather than duplicate this code, what if we make it available to all the images to be drawn?

This drawing code seems integral to the main program, so it could go into "src/main.rs", but we didn't really want to clutter up that file, so we created "src/extras.rs" to "hold the overflow". That seems like a good place to put this code.

Let's move ("edit/cut") the entire "draw_d51()" function out of "src/d51.rs" into ("edit/paste") "src/extras.rs". It doesn't really matter where it goes, but I pasted the "draw_d51() function at the bottom of the "src/extras.rs" file.

We can also move the "my_mvaddstr()" function. Again, it can go anywhere in the "src/extras.rs" file, but I pasted it at the bottom.

Now that all the ncurses code and sleep code has moved, we no longer need those "use" lines in "src/d51.rs", but we do need them in "src/extras.rs", so let's move those lines also. Just put them near the top of the file.

If we did our moving correctly, there are only two changes we need to make to our code for it to still do what it did before. The first is to make the "get_d51()" function public, so that the code that is now in another file ("extras.rs") can find it:

d51.rs
...
pub fn get_d51() -> Vec<Vec<String>> {
...

The second change is to tell the "src/extras.rs" file to "use" the "d51" module:

extras.rs
/* extras.rs */

use ncurses::*;
use std::{thread, time::Duration};
use crate::d51::*;
...

This time, we weren't able to just say use d51::*;, because of the way the module system works in Rust. Instead, we told Rust to start at the top of the crate in which we're working, which you can think of for now as the top of the project. That's not a very good explanation, and isn't particularly accurate, but hopefully it'll suffice for now.

Now the program should compile and run just as it did before. We haven't gained any functionality.

But now, instead of duplicating the code of "draw_d51()" into a new function named "draw_c51()", we can make it a little more generic and use the same piece of code to draw either of the trains.

Let's change the name of the function:

extras.rs
pub fn draw_d51draw_image(delay: u64, fly:bool, oops:bool) {
  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.
    
  let d51image = get_d51();
  let frames = d51image.len() as i32;            // How many animation cel frames in this image?
  let length = d51image[0][0].len() as i32;      // How long (width in screen columns/chars) is the train?
  let height = d51image[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 frame = frames - 1;               // Let's start with the highest-numbered frame, & 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_frame=&d51image[frame as usize]; // Strings are referenced by a usize, so here's another conversion.
    for each_line in current_frame {
      my_mvaddstr(line, col, &each_line, screen_width, screen_height);   // Display each line of the frame.
      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.
    frame = frame - 1;                      // Prepare to display next frame.
    if frame < 0 {                          // If we've gotten down to last frame...
      frame = frames - 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 frame will do it for us.
        row -=1;                            // ... raise the next drawing one row up.
      }
    }
  }

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

And we need to change "main()". Earlier, we had thought that in the "main()" function we'd choose which drawing routine to use, but now, since we will only have one drawing routine, rather than selecting among several drawing routines in "main()", we just need to pass the "kind" to the drawing routine, and let that routine sort it out.

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

    match kind.as_str() {                       
      "D51" => draw_d51(speed, fly, oops),
      "C51" => draw_c51(),
      &_ => println!("nope"),
    }
  draw_image(speed, fly, kind, oops);
} // end of main()

This makes for a much simpler "main()". Now we need to work on the "draw_image()" routine a little bit.

extras.rs
pub fn draw_image(delay: u64, fly:bool, kind:String, oops:bool) {            // The function is now receiving four items, including the "kind" of image to draw.
    // Start ncurses, initializing "stdscr()".
    initscr();

    // Don't echo keypresses to screen.
    noecho();

    // Turn off the display of the cursor.
    curs_set(CURSOR_VISIBILITY::CURSOR_INVISIBLE);

    // Get the screen dimensions.
    let mut screen_height = 0;
    let mut screen_width = 0;
    getmaxyx(stdscr(), &mut screen_height, &mut screen_width);

  let image: Vec<Vec^lt;String>> = get_d51();

  // Load ASCII-art into "image" variable (a vector of vectors holding Strings).
  let image = match kind.as_str() {                                               // Which image are we supposed to draw?
    "D51" => get_d51(),                                                           // D51 is the only one that currently works.
    // "C51" => get_c51(),
    &_ => {                                                                       // If the user enters an unknown image name (say, "racecar") ...
            println!("Invalid kind. You entered '{}'; the default was used instead.", kind);
            get_d51()
          },
  };
  
    let height = image[0].len() as i32; // How tall (in rows) is the D51image?
    let mut row = screen_height - height; // Put the wheelsbottom of image at the bottom of the screen.
    let count_of_inner_vecs: usize = image.len(); // How many animation cel frames in this image?
    let length: i32 = image[0][0].len() as i32; // How long (width in screen columns/chars) is the trainimage?
    let ms = Duration::from_millis(delay); // This is the "delay" converted to a format for the "sleep()" function below.

    let mut current_inner_vec: usize = count_of_inner_vecs - 1; // Count down from highest-numbered frame; then repeat cycle.

    for col in ((-length)..screen_width).rev() {
        // Count down from right-edge until nose of trainleading edge of image drags all of trainimage off left-edge.
        let current_frame = &image[current_inner_vec]; // Of the various inner_vecs, get the one we're drawing into its own variable.
        let mut line = row; // Keep "row" unaltered unless we're flying; use "line" within loop.
        for each_line in current_frame {
            my_mvaddstr(line, col, &each_line, screen_width, screen_height);
            line = line + 1;
        }
        refresh(); // Necessary to "bring to the forefront" the drawing we just did "in the back staging area".

        /* Pause so our eyes can see the frame. */
        thread::sleep(ms);
        // row = screen_height - height; // Reset row back to starting row.

        if current_inner_vec > 0 {
            current_inner_vec = current_inner_vec - 1; // Prepare to display next frame.
        } else {
            // we've gotten down to last frame, so ...
            current_inner_vec = count_of_inner_vecs - 1; // ... start over.
            if fly {
                // And if we're flying ...
                row -= 1; // ... fly up one row, and ...
                line = row + height; // ... erase last (bottom) line drawn, using enough repeated spaces for trainimage's length.
                my_mvaddstr(
                    line,
                    col,
                    &" ".repeat(length as usize),
                    screen_width,
                    screen_height,
                );
                // No need to refresh(); next frame will do it for us.
            }
        }
    }
    /* Once finished, terminate ncurses. */
    endwin();
} // end of draw_image()

The above should compile and run, but will only show the D51 train. In order to use this one function in order to draw any of the images, the images must be in the same format. In the case of the D51, we encoded that image directly into a vector of vectors in the "get_d51()" function in the "/src/d51.rs" file. In the case of the C51, we're going to do that a bit differently, by having the "get_c51()" function read the images from a text file into a vector of vectors. Let's work on that function now.

Read the C51 Image from a Text File

From earlier, you should already have a "src/c51.txt" file with the six frame-set images of the C51 train.

You should also have a minimilistic "src/c51.rs" file that does nothing but print "This is the 'c51' module." That no longer needs to be a "draw" function, as we have moved that function to "draw_image()"; instead, we need to rename the function to "get", which will return a vector of vector of Strings:

c51.rs
/* c51.rs */

pub fn draw_c51() {
  println!("This is the 'c51' module.");
pub fn get_c51() -> Vec<Vec<String>> {
   /*  This function reads a text file containing a series of ASCII-art image "frames". This gets put into a
        variable 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
        pushed onto an inner vector, named "inner_vec[the number of the vec]". When a blank line is found, we
        skip on to the next loop, and create a new inner vector at the same time, and then repeat the above
        process. Eventually, each of the image frames is put into its own inner vector, and all those inner
        vectors are put into an outer vector named "outer_vec".
    */

    let mut outer_vec: Vec<Vec<String>> = Vec::new(); // Outer vec holding all the inner frame vecs.

    // Read the file containing the image, into a variable named "image_string".
    let image_string = std::fs::read_to_string("src/c51.txt").expect("Error opening file.");

    let mut inner_vec: usize = 0; // To keep track of which frame/inner vector we're working with.
    
    outer_vec.push(Vec::new()); // Create first inner frame vec and push it into outer vec.

    // Process each line of the text file that's been read into memory.
    for each_line in image_string.lines() { // Break-up the single String variable "image_string" at newlines.
        if each_line != "" {
            // If the line is not blank,
            outer_vec[inner_vec].push(each_line.to_string()) // copy the line into the current inner frame vec.
        } else {
            // Else, if the line is blank, then we're between frames, so ignore the blan line, and
            inner_vec += 1; // prep for a new inner frame vec, and then
            outer_vec.push(Vec::new()); // create that new inner frame vec,
        } // before looping around to the next line.
    }
    
    return outer_vec;

} // end of drawget_c51()

Hopefully the comments, and the experience you've had programing in this tutorial up until now, will make the above code understandable to you.

If you compile and run this code, you discover that it almost works.

Image of C51 train leaving a trail of bits of pieces of itself.

Clean Up the Image Read From File

There seems to be three problems with our run of the C51 train. One is that the wheels seem ... wrong. Two is that there are quote-marks on both sides of the train. And the third is that the train leaves a huge trail.

We found out earlier that we can delete a trail by drawing a space on the tail end of the image being drawn. We can have our "get_image()" function replace the quote-marks with spaces; this will take care of two of the problems at once. We could edit the "c51.txt" file itself to replace the quote-marks, but then the author of such images would not know where his image ends; better to leave the quote marks in the original image, and deal with them after our function has read the file. This is easy enough:

c51.rs
...
   // Process each line of the text file that's been read into memory.
    for each_line in image_string.lines() {                   // Break-up the single String variable "image_string" at newlines.
        if each_line != "" {                                  // If the line is not blank,
            outer_vec[inner_vec].push(each_line.to_string())  // copy the line into the current inner frame vec.
            let line_string = each_line.replace("\"", " ").to_string(); // replace quotes with spaces, & convert the &str variable to a String variable
            outer_vec[inner_vec].push(line_string);             // Then copy that String into the current inner frame vec.
        } else {
            // Else, if the line is blank, then we're between frames, so ignore the blank line, and
            inner_vec += 1; // prep for a new inner frame vec, and then
            outer_vec.push(Vec::new()); // create that new inner frame vec,
        } // before looping around to the next line.

Try running that version, and there should only be remaining the single problem of the wheels not looking right.

It turns out that the "read_to_string()" function in "std::fs::" does not have a problem reading backslash ("\") characters. So use your favorite editor to search for and replace all double-backslashes ("\\") in your "c51.txt" file with single-backslashes ("\"), and see what that does for you. It works for me!

Now we have two trains that can get animated, and we're using two different methods for doing so. If this second method works as well as we think it does, it should be a very easy matter to add another train, say, the "Little" train. We can basically just copy "c51.rs" to "little.rs", and create an ASCII-art file, "little.txt", and be good to go.

But..., wait a sec. Then we're duplicating a lot of the same code. That's one of the purposes of a function, to avoid duplicating code. This same code could be used to read other image files, not just the "c51.txt" file. We could make it more general, and place it in "src/extras.rs". Let's do that; let's make a Read ASCII-Art from a Text File function in the next lesson.