summaryrefslogtreecommitdiff
path: root/src/main.rs
blob: 3e37ce2516e239686d61b6f84e534220c6f30751 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
use anyhow::{bail, Context, Result};
use app_state::StateFileStatus;
use clap::{Parser, Subcommand};
use std::{
    io::{self, BufRead, StdoutLock, Write},
    path::Path,
    process::exit,
};

use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit};

mod app_state;
mod cargo_toml;
mod cmd;
mod dev;
mod embedded;
mod exercise;
mod info_file;
mod init;
mod list;
mod progress_bar;
mod run;
mod terminal_link;
mod watch;

const CURRENT_FORMAT_VERSION: u8 = 1;
const DEBUG_PROFILE: bool = {
    #[allow(unused_assignments, unused_mut)]
    let mut debug_profile = false;

    #[cfg(debug_assertions)]
    {
        debug_profile = true;
    }

    debug_profile
};

// The current directory is the official Rustligns repository.
fn in_official_repo() -> bool {
    Path::new("dev/rustlings-repo.txt").exists()
}

fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
    stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J")
}

/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
#[derive(Parser)]
#[command(version)]
struct Args {
    #[command(subcommand)]
    command: Option<Subcommands>,
    /// Manually run the current exercise using `r` or `run` in the watch mode.
    /// Only use this if Rustlings fails to detect exercise file changes.
    #[arg(long)]
    manual_run: bool,
}

#[derive(Subcommand)]
enum Subcommands {
    /// Initialize the official Rustlings exercises
    Init,
    /// Run a single exercise. Runs the next pending exercise if the exercise name is not specified
    Run {
        /// The name of the exercise
        name: Option<String>,
    },
    /// Reset a single exercise
    Reset {
        /// The name of the exercise
        name: String,
    },
    /// Show a hint. Shows the hint of the next pending exercise if the exercise name is not specified
    Hint {
        /// The name of the exercise
        name: Option<String>,
    },
    /// Commands for developing (third-party) Rustlings exercises
    #[command(subcommand)]
    Dev(DevCommands),
}

fn main() -> Result<()> {
    let args = Args::parse();

    if !DEBUG_PROFILE && in_official_repo() {
        bail!("{OLD_METHOD_ERR}");
    }

    match args.command {
        Some(Subcommands::Init) => {
            if DEBUG_PROFILE {
                bail!("Disabled in the debug build");
            }

            {
                let mut stdout = io::stdout().lock();
                stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?;
                stdout.flush()?;
                io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
                stdout.write_all(b"\n")?;
            }

            return init::init().context("Initialization failed");
        }
        Some(Subcommands::Dev(dev_command)) => return dev_command.run(),
        _ => (),
    }

    if !Path::new("exercises").is_dir() {
        println!("{PRE_INIT_MSG}");
        exit(1);
    }

    let info_file = InfoFile::parse()?;

    if info_file.format_version > CURRENT_FORMAT_VERSION {
        bail!(FORMAT_VERSION_HIGHER_ERR);
    }

    let (mut app_state, state_file_status) = AppState::new(
        info_file.exercises,
        info_file.final_message.unwrap_or_default(),
    )?;

    // Show the welcome message if the state file doesn't exist yet.
    if let Some(welcome_message) = info_file.welcome_message {
        match state_file_status {
            StateFileStatus::NotRead => {
                let mut stdout = io::stdout().lock();
                clear_terminal(&mut stdout)?;

                let welcome_message = welcome_message.trim();
                write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?;
                stdout.flush()?;

                io::stdin().lock().read_until(b'\n', &mut Vec::new())?;

                clear_terminal(&mut stdout)?;
            }
            StateFileStatus::Read => (),
        }
    }

    match args.command {
        None => {
            let notify_exercise_names = if args.manual_run {
                None
            } else {
                // For the the notify event handler thread.
                // Leaking is not a problem because the slice lives until the end of the program.
                Some(
                    &*app_state
                        .exercises()
                        .iter()
                        .map(|exercise| exercise.name.as_bytes())
                        .collect::<Vec<_>>()
                        .leak(),
                )
            };

            loop {
                match watch::watch(&mut app_state, notify_exercise_names)? {
                    WatchExit::Shutdown => break,
                    // It is much easier to exit the watch mode, launch the list mode and then restart
                    // the watch mode instead of trying to pause the watch threads and correct the
                    // watch state.
                    WatchExit::List => list::list(&mut app_state)?,
                }
            }
        }
        Some(Subcommands::Run { name }) => {
            if let Some(name) = name {
                app_state.set_current_exercise_by_name(&name)?;
            }
            run::run(&mut app_state)?;
        }
        Some(Subcommands::Reset { name }) => {
            app_state.set_current_exercise_by_name(&name)?;
            let exercise_path = app_state.reset_current_exercise()?;
            println!("The exercise {exercise_path} has been reset");
        }
        Some(Subcommands::Hint { name }) => {
            if let Some(name) = name {
                app_state.set_current_exercise_by_name(&name)?;
            }
            println!("{}", app_state.current_exercise().hint);
        }
        // Handled in an earlier match.
        Some(Subcommands::Init | Subcommands::Dev(_)) => (),
    }

    Ok(())
}

const OLD_METHOD_ERR: &str = "You are trying to run Rustlings using the old method before v6.
The new method doesn't include cloning the Rustlings' repository.
Please follow the instructions in the README:
https://github.com/rust-lang/rustlings#getting-started";

const FORMAT_VERSION_HIGHER_ERR: &str =
    "The format version specified in the `info.toml` file is higher than the last one supported.
It is possible that you have an outdated version of Rustlings.
Try to install the latest Rustlings version first.";

const PRE_INIT_MSG: &str = r"
       Welcome to...
                 _   _ _
  _ __ _   _ ___| |_| (_)_ __   __ _ ___
 | '__| | | / __| __| | | '_ \ / _` / __|
 | |  | |_| \__ \ |_| | | | | | (_| \__ \
 |_|   \__,_|___/\__|_|_|_| |_|\__, |___/
                               |___/

The `exercises` directory wasn't found in the current directory.
If you are just starting with Rustlings, run the command `rustlings init` to initialize it.";