Let's start by creating a new project.
$ cd ~/projects/RUST $ cargo new sl
Now is as good of a time as any to tell Cargo about curses and Clap:
$ cd sl $ cargo add clap --features derive $ cargo add ncurses
You can alternatively specify the version to add, like so: $ cargo add clap@4.0.29 --features derive
and $ cargo add ncurses@5.101.0
. For me, at the time of this writing, the above commands netted me version 4.1.13 of clap, and version 5.101.0 of ncurses.
Now, compile your new "sl" project ($ cargo run
); this will download all the pieces for Clap and ncurses, and then it should print "Hello, World!" Note that nothing in this program is using clap or ncurses, but it's ready to do so, being that Cargo now knows about and has the pieces for these two add-on crates.
$ cargo run Updating crates.io index Compiling version_check v0.9.4 Compiling proc-macro2 v1.0.47 Compiling unicode-ident v1.0.5 Compiling quote v1.0.21 Compiling libc v0.2.138 Compiling io-lifetimes v1.0.3 Compiling syn v1.0.105 Compiling rustix v0.36.5 Compiling bitflags v1.3.2 Compiling linux-raw-sys v0.1.3 Compiling heck v0.4.0 Compiling os_str_bytes v6.4.1 Compiling strsim v0.10.0 Compiling once_cell v1.16.0 Compiling termcolor v1.1.3 Compiling proc-macro-error-attr v1.0.4 Compiling proc-macro-error v1.0.4 Compiling clap_lex v0.3.0 Compiling is-terminal v0.4.1 Compiling clap_derive v4.0.21 Compiling clap v4.0.29 Compiling sl-rust v0.1.0 (/home/kent/projects/sl/sl-rust) Finished dev [unoptimized + debuginfo] target(s) in 11.52s Running `target/debug/sl-rust` Hello, world!
In the Download the Source Code for "sl" lesson, we downloaded the C source code for "sl" to "~/projects/C/sl". That contains the ASCII-artwork of the various trains. along with the basic program logic we'll be implementing in our new Rust version of the "sl" project. If you look in the "sl.h" file (less ~/projects/C/sl/sl-5.02/sl.h
), you'll see the ASCII-art definitions of the various train cars that are used in "sl"'s animation, like so:
/*======================================== * sl.h: SL version 5.02 * Copyright 1993,2002,2014 * Toyoda Masashi * (mtoyoda@acm.org) * Last Modified: 2014/06/03 *======================================== */ #define D51HIGHT 10 #define D51FUNNEL 7 #define D51LENGTH 83 #define D51PATTERNS 6 #define D51STR1 " ==== ________ ___________ " #define D51STR2 " _D _| |_______/ \\__I_I_____===__|_________| " #define D51STR3 " |(_)--- | H\\________/ | | =|___ ___| " #define D51STR4 " / | | H | | | | ||_| |_|| " #define D51STR5 " | | | H |__--------------------| [___] | " #define D51STR6 " | ________|___H__/__|_____/[][]~\\_______| | " #define D51STR7 " |/ | |-----------I_____I [][] [] D |=======|__ " #define D51WHL11 "__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ " #define D51WHL12 " |/-=|___|= || || || |_____/~\\___/ " #define D51WHL13 " \\_/ \\O=====O=====O=====O_/ \\_/ " #define D51WHL21 "__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ " #define D51WHL22 " |/-=|___|=O=====O=====O=====O |_____/~\\___/ " #define D51WHL23 " \\_/ \\__/ \\__/ \\__/ \\__/ \\_/ "
And if you look in the "sl.c" main-program file, you'll see this piece of code:
static char *d51[D51PATTERNS][D51HIGHT + 1] = {{D51STR1, D51STR2, D51STR3, D51STR4, D51STR5, D51STR6, D51STR7, D51WHL11, D51WHL12, D51WHL13, D51DEL}, {D51STR1, D51STR2, D51STR3, D51STR4, D51STR5, D51STR6, D51STR7, D51WHL21, D51WHL22, D51WHL23, D51DEL}, {D51STR1, D51STR2, D51STR3, D51STR4, D51STR5, D51STR6, D51STR7, D51WHL31, D51WHL32, D51WHL33, D51DEL}, {D51STR1, D51STR2, D51STR3, D51STR4, D51STR5, D51STR6, D51STR7, D51WHL41, D51WHL42, D51WHL43, D51DEL}, {D51STR1, D51STR2, D51STR3, D51STR4, D51STR5, D51STR6, D51STR7, D51WHL51, D51WHL52, D51WHL53, D51DEL}, {D51STR1, D51STR2, D51STR3, D51STR4, D51STR5, D51STR6, D51STR7, D51WHL61, D51WHL62, D51WHL63, D51DEL}}; static char *coal[D51HIGHT + 1] = {COAL01, COAL02, COAL03, COAL04, COAL05, COAL06, COAL07, COAL08, COAL09, COAL10, COALDEL}; ...
The above code can be re-organized to look like the following:
static char *d51[D51PATTERNS][D51HIGHT + 1] = { { D51STR1, D51STR2, D51STR3, D51STR4, D51STR5, D51STR6, D51STR7, D51WHL11, D51WHL12, D51WHL13, D51DEL }, { D51STR1, D51STR2, D51STR3, D51STR4, D51STR5, D51STR6, D51STR7, D51WHL21, D51WHL22, D51WHL23, D51DEL }, ... and so on for the other four frames of the D51 train image
The "#define" statements tell the C compiler that wherever it sees the symbol (or name) "D51STR1", it should replace that symbol with the text string, " ==== ________ ___________ ". It's sort of like what other programming languages call a constant. If you don't understand all this, don't worry about it; basically just think of "D51STR1" as a place-holder for the text string of " ==== ________ ___________ ". In other words, when the compiler substites for the "#define" statements, the code above becomes the following:
static char *d51[D51PATTERNS][D51HIGHT + 1] = { { " ==== ________ ___________ ", " _D _| |_______/ \\__I_I_____===__|_________| ", " |(_)--- | H\\________/ | | =|___ ___| ", " / | | H | | | | ||_| |_|| ", " | | | H |__--------------------| [___] | ", " | ________|___H__/__|_____/[][]~\\_______| | ", " |/ | |-----------I_____I [][] [] D |=======|__ ", "__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ", " |/-=|___|= || || || |_____/~\\___/ ", " \\_/ \\O=====O=====O=====O_/ \\_/ ", " " }, { " ==== ________ ___________ ", " _D _| |_______/ \\__I_I_____===__|_________| ", " |(_)--- | H\\________/ | | =|___ ___| ", " / | | H | | | | ||_| |_|| ", " | | | H |__--------------------| [___] | ", " | ________|___H__/__|_____/[][]~\\_______| | ", " |/ | |-----------I_____I [][] [] D |=======|__ ", "__/ =| o |=-~~\\ /~~\\ /~~\\ /~~\\ ____Y___________|__ ", " |/-=|___|=O=====O=====O=====O |_____/~\\___/ ", " \\_/ \\__/ \\__/ \\__/ \\__/ \\_/ ", " " }, ... and so on with the other four frames of the D51 train image
The difference between the two frames are the three wheel-set lines in each frame. There are four more sets of wheels, to make a total of six animation frames. These six images of the train are then put into one variable named "*d51", which is a "static" (or "immutable" in Rust-speak) array of "char"acter items.
We can do something similar in Rust. We can create six "vectors" (a vector is known in some other programming languages as a one-dimensional array) of &str-type strings, and then put those six vectors into a containing vector, to create a "vector of &str vectors". This will do the same thing as the C-version's array. We'll look at that in just a minute. But first, let's look at just a single frame of the D51 train image, stored in a single vector:
When you run this (via "$ cargo run
"), you should see displayed the one frame of the train, like this:
westk@westk:~/projects/RUST/sl$ cargo run Compiling sl v0.1.0 (/home/westk/projects/RUST/sl) Finished dev [unoptimized + debuginfo] target(s) in 0.38s Running `target/debug/sl` ==== ________ ___________ _D _| |_______/ \__I_I_____===__|_________| |(_)--- | H\________/ | | =|___ ___| / | | H | | | | ||_| |_|| | | | H |__--------------------| [___] | | ________|___H__/__|_____/[][]~\_______| | |/ | |-----------I_____I [][] [] D |=======|__ __/ =| o |=-~~\ /~~\ /~~\ /~~\ ____Y___________|__ |/-=|___|= || || || |_____/~\___/ \_/ \O=====O=====O=====O_/ \_/ westk@westk:~/projects/RUST/sl$
But compare the backs of the train images; in the code they are jagged, but in the display they are straight. This is because some of the drawing characters are backslashes (\
), and usually backslashes are interpreted, or "escaped", to mean that the next character means something special. For example, the code \n
usually means to print a "newline" (like hitting the ENTER key). By prefixing the backslash with a second backslash, Cargo knows to treat the double-backslash as a normal backslash character instead of as an "escape" backslash.
We can tell Cargo to ignore the backslashes as being "escape backslashes", by telling Cargo to read a string in "raw" mode, by simply placing an "r" at the beginning of the string that needs to be read raw. This then allows us to remove the excess backslashes, making our code drawing identical to our desired output:
which then looks like:
Note that it is unnecessary to have the "r" on the lines that don't need them, but I included them just for the sake of consistency.
Also, just to line up the vector as a unit, let me move the "vec!{" portion of the variable declaration to the next line:
For more frames, we'll need more vectors. And we'll put that collection of vectors into an outer container vector (which will then become the named variable, which name I have changed below). That declaration looks like this:
And then to add the second frame of the image, we'll need to add a second vector:
Previously we were printing a single vector; now we're printing a vector of vectors, so we'll need to modify our printing routine:
(The resulting vector of &str vectors is now essentially a two-dimensional array.)
Here's the whole code:
To add more frames, we just add more vectors, like so:
Just FYI, you can use the "raw" mode anywhere you use text, like so:
println!("¯\ \_(ツ)_/¯");
vs
println!(r "¯\_(ツ)_/¯");
The above setup works great, as long as the "d51" definition stays within the "main()" function. But if it gets moved out of the "main()" function, like by putting it into a separate file (like the "sl.h" file is different to the "sl.c" file in the C version of the program), then "scope" issues come into play. Let's see what this looks like in real life.
Create a new file in your project's "src" directory named "images.rs" (".../sl/src/images.rs"), and move the "let d51..." definition out of "main.rs/main()" and into this new file:
If you try to compile this, you'll run into all sorts of problems.
The first problem is that this new file now constitutes a new "module" in our project, and the "main.rs/main()" part of the program doesn't yet know about this module. So we have to tell "main.rs" about this new module:
And we need to tell "main.rs" to search the "images" module for any symbols it does not recognize:
The next problem is that the compiler doesn't like "let" being used to define a global variable. This variable is "global", because it's not defined within any function. Earlier it was in the "main()" function, and was visible ("in scope") only to that function; now it's "out in the open" (although in a separate module/file), and not in a function, so it becomes "global", visible to the whole project (except, as mentioned, the compiler doesn't like a global "let"-type value).
So we have to convert it to something other than a "let"-style of variable. The compiler suggest using "static" or "const". So let's try that:
Now the compiler complains that "d51" is inaccessible. Although "main.rs" now knows about the module (because of "mod images;") and it knows to search this module (because of the "use images::*;"), it can't access the "d51" value because definitions like this default to being private to the module/file in which they are defined. To make "d51" "visible" ("in scope") to other pieces of our project than just the "images.rs" file, we have to add "pub" to the definition to make it public:
And that leads to a different error. Allocating a vector (filling it in) is not allowed in a "static" (or a "const" - try it yourself and see).
Bummer.
Our solution is to not define "d51" as a vector of &str vectors, but to define it as a simple &str string, and to let the program convert it later into a vector of vectors. That then would look like this:
Note that the name of the symbol is now upper-cased ("D51" instead of "d51"); it is a convention of Rust that constants are upper-cased. If you use lower-case, the compiler will complain, but will go ahead and compile anyway.
Note that now there is only one string for both frames, whereas before there was a string for each line of each frame. Our program will have to compensate for that. Here's how we'll compensate at the moment:
Note that each frame is separated from the previous frame by a blank line. Note that this line is not of the same length as the other image lines, and that the last line of each frame is line of spaces, not a blank line.
Finally, because of the way our program will handle these images later, note that each line of the image needs to be the same length, just as before. But without the double-quotes that's not easy to see. We can add a single-quote at the end of each line to make each line visible, as I did in the second frame above and on some lines in the third frame, or just take care to end each line at the appropriate place. (We can have our program strip the single-quotes out later so they don't display in the final result; for now, we'll not worry about them.)
Be aware that some editors may trim off what it deems unnecessary trailing white space. That can probably be turned off in your editor. Also, many editors will allow you to display white space (in nano, Alt-p will probably toggle that feature on or off); this will allow you to eyeball where your drawing starts and stops, and will look something like this (notice the second frame still has the optional ending single-quotes, and the third still has them on some lines):
By definition, a "const" in Rust contains "static" values, whereas an &str varaiable that is not declared as "const" is not "static" (without additional steps taken, that is). For our purposes here, you can think of a "static" value as one that never "dies", whereas a non-static value "dies" as soon as it the section of the program in which it is declared "dies". This is why a &str variable can not be returned from a function; once the function finishes, and tries to ruturn the &str value, that &str value also "dies", and can't be returned from the function. But a "const" is by definition "static", and therefore doesn't "die" when the focus of the program leaves the "images.rs" module.
An advantage of using a "const" composed of a single string is that this makes the drawing of the image easier on the artist, as s/he doesn't have to worry about adding all the extra trappings (extra "r"s, and quote-marks, and vec declarations, etc) of the other methods. The single-quotes at the end of each line are not even required; they are only there to help the ASCII-artist know where is the end of the line. Our program will remove them at the appropriate time.
There is another method of getting the image data from a separate file into our "main()" program, using the "include!" macro, which will include the contents of an external file. Honestly, I'm not sure of the pros and cons of this method, but I get the feeling it's not the best idea. But here's how that would look (note that the name/location of the external file is up to you):
If we used this "include!" macro method, we'd have to create a separate file for each set of images (one for the D51, one for the C51, one for the Little train, etc). There's pros and cons to that, but for now, we probably want to put all our images into a single separate file. So in addition to putting the D51 image frames into a separate file, "src/images.rs", let's also Put All Our Graphic Images Into A Separate File.