Full Code Listing Up to the "Plan C - Add More Drawings" Page

Return back to the "Plan C - Add More Drawings" Page

main.rs
/* main.rs */

mod drawing;
mod get_options;
mod images;

use drawing::*;
use get_options::*;

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

    // Convert image_string to a vector of String vectors
    let mut image_vecvec = string_to_stringvecvec(&image_string);

    // If we have a tail for the main image, such as a coalcar, do likewise to it, and then stitch the two images together.
    if tail_string != "".to_string() {
        let tail_vecvec = string_to_stringvecvec(&tail_string);
        image_vecvec = stitch_tail_to_body(image_vecvec, tail_vecvec);
    }
    // Animate the resulting image vector.
    draw_image(speed, fly, kind, oops, image_vecvec);
} // end of main()
images.rs
/* images.rs

 This project is a conversion of the "sl" application as found in Debian "Bullseye",
 from the C programming language to Rust. The copyright blurb of the original is below:

*========================================
*    sl.h: SL version 5.02
*      Copyright 1993,2002,2014
*                Toyoda Masashi
*                (mtoyoda@acm.org)
*      Last Modified: 2014/06/03
*========================================

 This conversion is based on a tutorial for this project, written by Kent West, 2023.

 To add an art image, just follow the pattern of the images below, making sure to give
 a unique name as the constant value, and making sure to put a blank line between each
 animation frame, and making sure to enclose each side of each line of each frame in
 single-quotes (which will be converted by the program into spaces). Then add an entry
 for your constant in both the documentation string of the "What kind of object" arg
 in the "struct Args" section of the "get_options.rs" file, and in the "Set 'kind'."
 section of the "parse_opts()" function in that same file.

 To add an optional "tail", such as the COALCAR, it must have the same number of rows
 as the body to which it will be attached, and then its entry must be added to the main
 body's entry in the "Set 'kind'" match statement of the "parse_opts()" function in the
 "get_otions.rs" file.
*/

pub const COALCAR: &str = r"
'                              '
'                              '
'                              '
'    _________________         '
'   _|                \_____A  '
' =|                        |  '
' -|                        |  '
'__|________________________|_ '
'|__________________________|_ '
'   |_D__D__D_|  |_D__D__D_|   '
'    \_/   \_/    \_/   \_/    '
"; // end of COALCAR

pub const D51: &str = r"
'                                                     '
'      ====        ________                ___________'
'  _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   |_____/~\___/       '
'  \_/      \__/  \__/  \__/  \__/      \_/           '

'                                                     '
'      ====        ________                ___________'
'  _D _|  |_______/        \__I_I_____===__|_________|'
'   |(_)---  |   H\________/ |   |        =|___ ___|  '
'   /     |  |   H  |  |     |   |         ||_| |_||  '
'  |      |  |   H  |__--------------------| [___] |  '
'  | ________|___H__/__|_____/[][]~\_______|       |  '
'  |/ |   |-----------I_____I [][] []  D   |=======|__'
'__/ =| o |=-O=====O=====O=====O \ ____Y___________|__'
' |/-=|___|=    ||    ||    ||    |_____/~\___/       '
'  \_/      \__/  \__/  \__/  \__/      \_/           '

'                                                     '
'      ====        ________                ___________'
'  _D _|  |_______/        \__I_I_____===__|_________|'
'   |(_)---  |   H\________/ |   |        =|___ ___|  '
'   /     |  |   H  |  |     |   |         ||_| |_||  '
'  |      |  |   H  |__--------------------| [___] |  '
'  | ________|___H__/__|_____/[][]~\_______|       |  '
'  |/ |   |-----------I_____I [][] []  D   |=======|__'
'__/ =| o |=-~O=====O=====O=====O\ ____Y___________|__'
' |/-=|___|=    ||    ||    ||    |_____/~\___/       '
'  \_/      \__/  \__/  \__/  \__/      \_/           '

'                                                     '
'      ====        ________                ___________'
'  _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/      \_/           '
"; // end of D51

pub const C51: &str = r"
'        ___                                            '
'       _|_|_  _     __       __             ___________'
'    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   |__|+-/~\_|     '
'\_/         \_/  \____/  \____/  \____/      \_/       '
"; // end of C51

pub const LITTLE: &str = r"
'     ++      +------ ____                 ____________________ '
'     ||      |+-+ |  |   \@@@@@@@@@@@     |  ___ ___ ___ ___ | '
'   /---------|| | |  |    \@@@@@@@@@@@@@_ |  |_| |_| |_| |_| | '
'  + ========  +-+ |  |                  | |__________________| '
' _|--O========O~\-+  |__________________| |__________________| '
'//// \_/      \_/       (O)       (O)        (O)        (O)    '

'     ++      +------ ____                 ____________________ ' 
'     ||      |+-+ |  |   \@@@@@@@@@@@     |  ___ ___ ___ ___ | '
'   /---------|| | |  |    \@@@@@@@@@@@@@_ |  |_| |_| |_| |_| | '
'  + ========  +-+ |  |                  | |__________________| '
' _|--/O========O\-+  |__________________| |__________________| '
'//// \_/      \_/       (O)       (O)        (O)        (O)    '

'     ++      +------ ____                 ____________________ '
'     ||      |+-+ |  |   \@@@@@@@@@@@     |  ___ ___ ___ ___ | '
'   /---------|| | |  |    \@@@@@@@@@@@@@_ |  |_| |_| |_| |_| | '
'  + ========  +-+ |  |                  | |__________________| '
' _|--/~O========O-+  |__________________| |__________________| '
'//// \_/      \_/       (O)       (O)        (O)        (O)    '

'     ++      +------ ____                 ____________________ '
'     ||      |+-+ |  |   \@@@@@@@@@@@     |  ___ ___ ___ ___ | '
'   /---------|| | |  |    \@@@@@@@@@@@@@_ |  |_| |_| |_| |_| | '
'  + ========  +-+ |  |                  | |__________________| '
' _|--/~\------/~\-+  |__________________| |__________________| '
'//// \_O========O       (O)       (O)        (O)        (O)    '

'     ++      +------ ____                 ____________________ '
'     ||      |+-+ |  |   \@@@@@@@@@@@     |  ___ ___ ___ ___ | '
'   /---------|| | |  |    \@@@@@@@@@@@@@_ |  |_| |_| |_| |_| | '
'  + ========  +-+ |  |                  | |__________________| '
' _|--/~\------/~\-+  |__________________| |__________________| '
'//// \O========O/       (O)       (O)        (O)        (O)    '

'     ++      +------ ____                 ____________________ '
'     ||      |+-+ |  |   \@@@@@@@@@@@     |  ___ ___ ___ ___ | '
'   /---------|| | |  |    \@@@@@@@@@@@@@_ |  |_| |_| |_| |_| | '
'  + ========  +-+ |  |                  | |__________________| '
' _|--/~\------/~\-+  |__________________| |__________________| '
'//// O========O_/       (O)       (O)        (O)        (O)    '
"; // end of LITTLE

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

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

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

'         '
' __0__   '
'/  |  \  '
'  / \    '
' _\ /_   '
"; // end of JACK
drawing.rs
/* drawing.rs */

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

fn my_mvaddstr(row: i32, mut col: i32, frame_line: &str, screen_width: i32, screen_height: i32) {
    /* This function recieves one line of an ASCII-art image, and
       trims away any of that line that would otherwise be displayed
       off-screen; then it displays the remaining portion of the
       line.
    */
    let mut line = frame_line.to_string(); // An &str is immutable by nature; we need a mutable string.

    // Add a space to end of line as eraser of leftovers from previous frame.
    line.push(' ');

    // Trim from left side as train moves off left edge
    if col < 0 {
        // If we've moved left of the left-edge,
        for _ in 0..col.abs() {
            // for each column beyond edge,
            line.remove(0); // trim the first char off the string, repeatedly.
        } // "line" should now have front end trimmed off.
        col = 0;
    }

    // Trim from right side if it extends off right edge
    if col + (line.len() as i32) > screen_width {
        let amt_to_trim = (col + (line.len() as i32)) - screen_width;
        for _ in 0..amt_to_trim {
            line.pop();
        }
    }

    if (row >= 0) && (row < screen_height) {
        // We're on-screen, so it's okay to print this line.
        mvaddstr(row, col, &line);
    }
} // end of my_mvaddstr()

pub fn draw_image(delay: u64, fly: bool, kind: String, oops: bool, image_vecvec: Vec<Vec<String>>) {
    // 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 height = image_vecvec[0].len() as i32; // How tall is the image?
    let mut row = screen_height - height; // Put the wheels at the bottom of the screen.
    let count_of_inner_vecs: usize = image_vecvec.len(); // How many animation cel frames in this image?
    let length: i32 = image_vecvec[0][0].len() as i32; // How long (width in screen columns/chars) is the train?
    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 train drags all of train off left-edge.
        let current_frame = &image_vecvec[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);

        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 check to see if we're flying. If so...
                row -= 1; //... raise the next drawing one row up, and ...
                          // ... erase last (bottom) line drawn, using enough repeated spaces for train's length.
                line = row + height;
                my_mvaddstr(
                    line,
                    col,
                    &" ".repeat(length as usize),
                    screen_width,
                    screen_height,
                );
                // If the entire height of the image is off the top edge of screen (top row of screen is 0)...
                if (row + height) < 0 {
                    // ... start drawing image at bottom of screen.
                    row = screen_height;
                }
                // No need to refresh(); next frame will do it for us.
            }
        }
    }
    /* Once finished, terminate ncurses. */
    endwin();
} // end of draw_image()
get_options.rs
/* get_options.rs */

use crate::images::*;

// Use c[ommand] l[ine] a[rgument] p[arser] to get command-line arguments.
use clap::Parser;

// This tells clap to derive info it needs from the struct we build below, from the "#[" lines..
#[derive(Parser)]

/* Define the argument inputs we might expect; put them in a struct named "Args". */
struct Args {
    /// Move the train '[[z]ip]py', '[f]ast', '[[m]ed]ium', '[s]low' or '[c]rawl'?
    #[arg(short, long, default_value_t = String::from("med"))]
    // The above tells clap that the next line is an argument, that can be entered in as "s" or "speed".
    speed: String,

    /// Fly?
    #[arg(short, long, default_value_t = false)]
    fly: bool,

    /// What Kind of object? D51 | C51 | Little | Jack | Boat | Plane
    #[arg(short, long, default_value_t = String::from("D51"))]
    kind: String,

    /// Oops?
    #[arg(short, long, default_value_t = false)]
    oops: bool,
} // end of Args definitions

pub fn parse_opts() -> (u64, bool, String, bool, String, String) {
    // Get command-line options using Clap.
    let switches: Args = Args::parse();

    // Set "speed".
    let speed: u64 = match (&switches.speed.to_uppercase()).as_str() {
        // The variable "switches.speed" is a String-type (as defined above in the "Args" struct). We upper-case
        // it, then compare it to possible matches which are string-slices (&str), which requires the ".as_str()"
        // conversion. We then "return" the selected value, which is assigned to "speed".
        "Z" | "ZIPPY" | "ZIP" | "U" | "ULTRAFAST" => 10,
        "F" | "FAST" => 20,
        "M" | "MED" | "MEDIUM" => 50,
        "S" | "SLOW" => 100,
        "C" | "CRAWL" => 400,
        _ => {
            println!("Invalid input; must be '[z]ippy/[u]ltrafast, '[f]ast', '[[m]ed]ium', '[s]low', or '[c]rawl'. You entered '{}', so the default of 'medium' was used.", switches.speed);
            50
        }
    };

    // Set "kind".
    let (kind, image_string, tail) = match (&switches.kind.to_uppercase()).as_str() {
        "D" | "D51" => ("D51".to_string(), D51.to_string(), COALCAR.to_string()),
        "C" | "C51" => (String::from("C51"), String::from(C51), COALCAR.to_string()),
        "L" | "LITTLE" => ("LITTLE".to_owned(), LITTLE.to_owned(), "".to_string()),
        "J" | "JACK" => (String::from("JACK"), JACK.to_string(), "".to_string()),
        "B" | "BOAT" => (String::from("BOAT"), D51.to_string(), "".to_string()),
        "P" | "PLANE" => (String::from("PLANE"), D51.to_string(), "".to_string()),
        _ => {
            // If none of them match, then ...
            println!("Invalid input; run program with '--help' option for more information. You entered '{}', so the default of 'd51' was used.", switches.kind);
            (String::from("D51"), D51.to_string(), COALCAR.to_string())
        }
    };

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

pub fn string_to_stringvecvec(image_str: &str) -> Vec<Vec<String>> {
    // Get rid of the single-quote-marks that delimit the side boundaries of the ASCII-art image.
    let image_string: String = image_str.replace("\n\'", "\n");
    let mut image_string: String = image_string.replace("\'\n", "\n");

    // Second, remove newline at start of image.
    image_string.remove(0);

    // Then, third, convert each frame in the string to a string element, then add the element to the vector:

    // - Create an empty parent vector.
    let mut outer_vec: Vec<Vec<String>> = Vec::new();

    // - Create a counter to track which inner vector/frame we're working with.
    let mut inner_vec: usize = 0;

    // - Create the first, empty, inner vector for the first image frame.
    outer_vec.push(Vec::new()); // Create first frame vec in outer vec, named "frames".

    // - Process each line of image_string, breaking that string at newlines, into separate lines.
    for each_line in image_string.lines() {
        // If the current line is not blank...
        if each_line != "" {
            // ... then push that line into the current inner vector.
            outer_vec[inner_vec].push(each_line.to_string());
        } else {
            // Else, if the line is blank, then we're between frames, so ignore the blank line,
            // and increment the count of inner vectors, and ...
            inner_vec += 1;
            // ... create a new inner vector for the next image frame...
            outer_vec.push(Vec::new());
        } // ... before looping around to the next line.
    }

    return outer_vec;
} // end of string_to_stringvecvec()

pub fn stitch_tail_to_body(body: Vec<Vec<String>>, tail: Vec<Vec<String>>) -> Vec<Vec<String>> {
    // Clone the main body vector.
    let mut image = body.clone();

    // How many frames in the main body?
    let num_of_frames = image.len();

    // Cycle through each frame, and stitch each row of tail to each row of each frame of main body.
    for frame in 0..num_of_frames {
        for (row, _each_line) in body[frame].iter().enumerate() {
            image[frame][row].push_str(&tail[0][row]);
        }
    }
    // Return the modified cloned image.
    image
} // end of stitch_tail_to_body()