summaryrefslogtreecommitdiff
path: root/src/tui.rs
blob: bb87365205899c97204e0fad2de4bfdf24fcfd57 (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
use anyhow::Result;
use crossterm::{
    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
    ExecutableCommand,
};
use notify_debouncer_mini::{new_debouncer, notify::RecursiveMode, DebouncedEventKind};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::{
    io::stdout,
    path::Path,
    sync::mpsc::{channel, RecvTimeoutError},
    time::Duration,
};

use crate::{
    exercise::Exercise,
    verify::{verify, VerifyState},
};

fn watch(exercises: &[Exercise]) -> Result<()> {
    let (tx, rx) = channel();

    let mut debouncer = new_debouncer(Duration::from_secs(1), tx)?;
    debouncer
        .watcher()
        .watch(Path::new("exercises"), RecursiveMode::Recursive)?;

    let mut failed_exercise_hint = match verify(exercises, (0, exercises.len()))? {
        VerifyState::AllExercisesDone => return Ok(()),
        VerifyState::Failed(exercise) => Some(&exercise.hint),
    };

    let mut pending_exercises = Vec::with_capacity(exercises.len());
    loop {
        match rx.recv_timeout(Duration::from_secs(1)) {
            Ok(event) => match event {
                Ok(events) => {
                    for event in events {
                        if event.kind == DebouncedEventKind::Any
                            && event.path.extension().is_some_and(|ext| ext == "rs")
                        {
                            pending_exercises.extend(exercises.iter().filter(|exercise| {
                                !exercise.looks_done().unwrap_or(false)
                                    || event.path.ends_with(&exercise.path)
                            }));
                            let num_done = exercises.len() - pending_exercises.len();

                            match verify(
                                pending_exercises.iter().copied(),
                                (num_done, exercises.len()),
                            )? {
                                VerifyState::AllExercisesDone => return Ok(()),
                                VerifyState::Failed(exercise) => {
                                    failed_exercise_hint = Some(&exercise.hint);
                                }
                            }

                            pending_exercises.clear();
                        }
                    }
                }
                Err(e) => println!("watch error: {e:?}"),
            },
            Err(RecvTimeoutError::Timeout) => {
                // the timeout expired, just check the `should_quit` variable below then loop again
            }
            Err(e) => println!("watch error: {e:?}"),
        }

        // TODO: Check if we need to exit
    }
}

pub fn tui(exercises: &[Exercise]) -> Result<()> {
    let mut stdout = stdout().lock();
    stdout.execute(EnterAlternateScreen)?;
    enable_raw_mode()?;
    let mut terminal = Terminal::new(CrosstermBackend::new(&mut stdout))?;
    terminal.clear()?;

    watch(exercises)?;

    drop(terminal);
    stdout.execute(LeaveAlternateScreen)?;
    disable_raw_mode()?;

    // TODO
    println!("We hope you're enjoying learning about Rust!");
    println!("If you want to continue working on the exercises at a later point, you can simply run `rustlings watch` again");

    Ok(())
}