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.
The first thing to do is to create a new "src/c51.rs" file, containing the following:
/* c51.rs */ pub fn draw_c51() { println!("This is the 'c51' module."); } // end of draw_c51()
Then edit "src/main()" to look like this:
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. $
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.
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:
" ___ " " _|_|_ _ __ __ ___________" " 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.
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:
...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 */ 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:
pub fndraw_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. letd51image = 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.
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.
pub fn draw_image(delay: u64, fly:bool,
let image: Vec<Vec^lt;String>> = get_d51();
D51the wheelstrainnose of traintraintrain
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.
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 */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 ofdrawget _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.
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:
... // 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.