Compare commits

...

7 commits

Author SHA1 Message Date
Nahor 9935560481
Merge d3f819f86f into e852e60416 2024-10-06 23:48:20 +08:00
Nahor d3f819f86f Add command line command to check all exercises 2024-10-04 14:36:36 -07:00
Nahor aa83fd6bc4 Show a progress bar when running check_all
Replace the "Progress: xxx/yyy" with a progress bar when checking all
the exercises
2024-10-02 15:28:42 -07:00
Nahor e2f7734f37 Limit the amount of parallelism in check_all
Don't create more threads than there are CPU cores.
2024-10-02 14:42:50 -07:00
Nahor 5c17abd1bf Use a channel to update the check_all progress
The previous code was checking the threads in the order they were
created. So the progress update would be blocked on an earlier thread
even if later thread were already done.

Add to that that multiple instances of `cargo build` cannot run in
parallel, they will be serialized instead. So if the exercises needs to
be recompiled, depending on the order those `cargo build` are run,
the first update can be a long time coming.

So instead of relying on the thread terminating, use a channel to get
notified when an exercise check is done, regardless of the order they
finish in.
2024-10-02 14:10:26 -07:00
Nahor c52867eb8b Add command to check all the exercises
This allows for skipping repeating "next" when multiple exercises
are done at once, or when earlier exercises have been updated/changed
(and thus must be redone) while still working of the whole set (i.e.
the final check_all is not yet available to flag those undone exercises)
2024-10-02 13:40:32 -07:00
Nahor 26fd97a209 Update all exercises during the final check
The previous code run the check on all exercises but only updates one
exercise (the first that failed) even if multiple failed. The user won't
be able to see all the failed exercises when viewing the list, and will
have to run check_all after each fixed exercise.

This change will update all the exercises so the user can see all that
failed, fix them all, and only then need run check_all again.
2024-10-02 11:45:55 -07:00
6 changed files with 331 additions and 67 deletions

View file

@ -1,10 +1,16 @@
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use crossterm::{
queue,
style::{Print, ResetColor, SetForegroundColor},
terminal,
};
use std::{ use std::{
env, env,
fs::{File, OpenOptions}, fs::{File, OpenOptions},
io::{self, Read, Seek, StdoutLock, Write}, io::{self, Read, Seek, StdoutLock, Write},
path::{Path, MAIN_SEPARATOR_STR}, path::{Path, MAIN_SEPARATOR_STR},
process::{Command, Stdio}, process::{Command, Stdio},
sync::{atomic::AtomicUsize, mpsc, Arc},
thread, thread,
}; };
@ -15,10 +21,11 @@ use crate::{
embedded::EMBEDDED_FILES, embedded::EMBEDDED_FILES,
exercise::{Exercise, RunnableExercise}, exercise::{Exercise, RunnableExercise},
info_file::ExerciseInfo, info_file::ExerciseInfo,
term, term::{self, progress_bar_with_success},
}; };
const STATE_FILE_NAME: &str = ".rustlings-state.txt"; const STATE_FILE_NAME: &str = ".rustlings-state.txt";
const DEFAULT_CHECK_PARALLELISM: usize = 8;
#[must_use] #[must_use]
pub enum ExercisesProgress { pub enum ExercisesProgress {
@ -35,10 +42,12 @@ pub enum StateFileStatus {
NotRead, NotRead,
} }
enum AllExercisesCheck { #[derive(Clone, Copy, PartialEq)]
Pending(usize), enum AllExercisesResult {
AllDone, Pending,
CheckedUntil(usize), Success,
Failed,
Error,
} }
pub struct AppState { pub struct AppState {
@ -270,18 +279,32 @@ impl AppState {
self.write() self.write()
} }
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> { // Set the status of an exercise without saving. Returns `true` if the
// status actually changed (and thus needs saving later)
pub fn set_status(&mut self, exercise_ind: usize, done: bool) -> Result<bool> {
let exercise = self let exercise = self
.exercises .exercises
.get_mut(exercise_ind) .get_mut(exercise_ind)
.context(BAD_INDEX_ERR)?; .context(BAD_INDEX_ERR)?;
if exercise.done { if exercise.done == done {
exercise.done = false; Ok(false)
self.n_done -= 1; } else {
exercise.done = done;
if done {
self.n_done += 1;
} else {
self.n_done -= 1;
}
Ok(true)
}
}
// Set the status of an exercise to "pending" and save
pub fn set_pending(&mut self, exercise_ind: usize) -> Result<()> {
if self.set_status(exercise_ind, false)? {
self.write()?; self.write()?;
} }
Ok(()) Ok(())
} }
@ -380,62 +403,173 @@ impl AppState {
} }
// Return the exercise index of the first pending exercise found. // Return the exercise index of the first pending exercise found.
fn check_all_exercises(&self, stdout: &mut StdoutLock) -> Result<Option<usize>> { pub fn check_all_exercises(
stdout.write_all(FINAL_CHECK_MSG)?; &mut self,
stdout: &mut StdoutLock,
final_check: bool,
) -> Result<Option<usize>> {
if !final_check {
stdout.write_all(INTERMEDIATE_CHECK_MSG)?;
} else {
stdout.write_all(FINAL_CHECK_MSG)?;
}
let n_exercises = self.exercises.len(); let n_exercises = self.exercises.len();
let status = thread::scope(|s| { let (mut checked_count, mut results) = thread::scope(|s| {
let handles = self let (tx, rx) = mpsc::channel();
.exercises let exercise_ind = Arc::new(AtomicUsize::default());
.iter()
.map(|exercise| {
thread::Builder::new()
.spawn_scoped(s, || exercise.run_exercise(None, &self.cmd_runner))
})
.collect::<Vec<_>>();
for (exercise_ind, spawn_res) in handles.into_iter().enumerate() { let num_core = thread::available_parallelism()
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?; .map_or(DEFAULT_CHECK_PARALLELISM, |count| count.get());
stdout.flush()?; (0..num_core).for_each(|_| {
let tx = tx.clone();
let exercise_ind = exercise_ind.clone();
let this = &self;
let _ = thread::Builder::new().spawn_scoped(s, move || {
loop {
let exercise_ind =
exercise_ind.fetch_add(1, std::sync::atomic::Ordering::AcqRel);
let Some(exercise) = this.exercises.get(exercise_ind) else {
// No more exercises
break;
};
let Ok(handle) = spawn_res else { // Notify the progress bar that this exercise is pending
return Ok(AllExercisesCheck::CheckedUntil(exercise_ind)); if tx.send((exercise_ind, None)).is_err() {
}; break;
};
let Ok(success) = handle.join().unwrap() else { let result = exercise.run_exercise(None, &this.cmd_runner);
return Ok(AllExercisesCheck::CheckedUntil(exercise_ind));
};
if !success { // Notify the progress bar that this exercise is done
return Ok(AllExercisesCheck::Pending(exercise_ind)); if tx.send((exercise_ind, Some(result))).is_err() {
break;
}
}
});
});
// Drop this `tx`, since the `rx` loop will not stop while there is
// at least one tx alive (i.e. we want the loop to block only while
// there are `tx` clones, i.e. threads)
drop(tx);
// Print the legend
queue!(
stdout,
Print("Color legend: "),
SetForegroundColor(term::PROGRESS_FAILED_COLOR),
Print("Failure"),
ResetColor,
Print(" - "),
SetForegroundColor(term::PROGRESS_SUCCESS_COLOR),
Print("Success"),
ResetColor,
Print(" - "),
SetForegroundColor(term::PROGRESS_PENDING_COLOR),
Print("Checking"),
ResetColor,
Print("\n"),
)
.unwrap();
// We expect at least a few "pending" notifications shortly, so don't
// bother printing the initial state of the progress bar and flushing
// stdout
let line_width = terminal::size().unwrap().0;
let mut results = vec![AllExercisesResult::Pending; n_exercises];
let mut pending = 0;
let mut success = 0;
let mut failed = 0;
while let Ok((exercise_ind, result)) = rx.recv() {
match result {
None => {
pending += 1;
}
Some(Err(_)) => {
results[exercise_ind] = AllExercisesResult::Error;
}
Some(Ok(true)) => {
results[exercise_ind] = AllExercisesResult::Success;
pending -= 1;
success += 1;
}
Some(Ok(false)) => {
results[exercise_ind] = AllExercisesResult::Failed;
pending -= 1;
failed += 1;
}
} }
write!(stdout, "\r").unwrap();
progress_bar_with_success(
stdout,
pending,
failed,
success,
n_exercises as u16,
line_width,
)
.unwrap();
stdout.flush()?;
} }
Ok::<_, io::Error>(AllExercisesCheck::AllDone) Ok::<_, io::Error>((success, results))
})?; })?;
let mut exercise_ind = match status { // If we got an error while checking all exercises in parallel,
AllExercisesCheck::Pending(exercise_ind) => return Ok(Some(exercise_ind)), // it could be because we exceeded the limit of open file descriptors.
AllExercisesCheck::AllDone => return Ok(None), // Therefore, re-try those one at a time (i.e. sequentially).
AllExercisesCheck::CheckedUntil(ind) => ind, results
}; .iter_mut()
.enumerate()
.filter(|(_, result)| {
**result == AllExercisesResult::Pending || **result == AllExercisesResult::Error
})
.try_for_each(|(exercise_ind, result)| {
let exercise = self.exercises.get(exercise_ind).context(BAD_INDEX_ERR)?;
*result = match exercise
.run_exercise(None, &self.cmd_runner)
.context("Sequential retry")
{
Ok(true) => AllExercisesResult::Success,
Ok(false) => AllExercisesResult::Failed,
Err(err) => bail!(err),
};
checked_count += 1;
write!(stdout, "\rProgress: {checked_count}/{n_exercises}")?;
stdout.flush()?;
Ok(())
})?;
// We got an error while checking all exercises in parallel. // Update the state of each exercise and return the first that failed
// This could be because we exceeded the limit of open file descriptors. let first_fail = results
// Therefore, try to continue the check sequentially. .iter()
for exercise in &self.exercises[exercise_ind..] { .enumerate()
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?; .filter_map(|(exercise_ind, result)| {
stdout.flush()?; match result {
AllExercisesResult::Success => self
.set_status(exercise_ind, true)
.map_or_else(|err| Some(Err(err)), |_| None),
AllExercisesResult::Failed => self
.set_status(exercise_ind, false)
.map_or_else(|err| Some(Err(err)), |_| Some(Ok(exercise_ind))),
// The sequential check done earlier will have converted all
// exercises to Success/Failed, or bailed, so those are unreachable
AllExercisesResult::Pending | AllExercisesResult::Error => unreachable!(),
}
})
.try_fold(None::<usize>, |current_min, index| {
match (current_min, index) {
(_, Err(err)) => Err(err),
(None, Ok(index)) => Ok(Some(index)),
(Some(current_min), Ok(index)) => Ok(Some(current_min.min(index))),
}
})?;
self.write()?;
let success = exercise.run_exercise(None, &self.cmd_runner)?; Ok(first_fail)
if !success {
return Ok(Some(exercise_ind));
}
exercise_ind += 1;
}
Ok(None)
} }
/// Mark the current exercise as done and move on to the next pending exercise if one exists. /// Mark the current exercise as done and move on to the next pending exercise if one exists.
@ -462,20 +596,24 @@ impl AppState {
stdout.write_all(b"\n")?; stdout.write_all(b"\n")?;
} }
if let Some(pending_exercise_ind) = self.check_all_exercises(stdout)? { if let Some(pending_exercise_ind) = self.check_all_exercises(stdout, true)? {
stdout.write_all(b"\n\n")?; stdout.write_all(b"\n\n")?;
self.current_exercise_ind = pending_exercise_ind; self.current_exercise_ind = pending_exercise_ind;
self.exercises[pending_exercise_ind].done = false; self.exercises[pending_exercise_ind].done = false;
// All exercises were marked as done.
self.n_done -= 1;
self.write()?;
return Ok(ExercisesProgress::NewPending); return Ok(ExercisesProgress::NewPending);
} }
// Write that the last exercise is done. // Write that the last exercise is done.
self.write()?; self.write()?;
self.render_final_message(stdout)?;
Ok(ExercisesProgress::AllDone)
}
pub fn render_final_message(&self, stdout: &mut StdoutLock) -> Result<()> {
clear_terminal(stdout)?; clear_terminal(stdout)?;
stdout.write_all(FENISH_LINE.as_bytes())?; stdout.write_all(FENISH_LINE.as_bytes())?;
@ -485,12 +623,14 @@ impl AppState {
stdout.write_all(b"\n")?; stdout.write_all(b"\n")?;
} }
Ok(ExercisesProgress::AllDone) Ok(())
} }
} }
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n"; const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n";
const INTERMEDIATE_CHECK_MSG: &[u8] = b"Checking all exercises
";
const FINAL_CHECK_MSG: &[u8] = b"All exercises seem to be done. const FINAL_CHECK_MSG: &[u8] = b"All exercises seem to be done.
Recompiling and running all exercises to make sure that all of them are actually done. Recompiling and running all exercises to make sure that all of them are actually done.
"; ";

View file

@ -1,6 +1,10 @@
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use app_state::StateFileStatus; use app_state::StateFileStatus;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use crossterm::{
style::{Color, Print, ResetColor, SetForegroundColor},
QueueableCommand,
};
use std::{ use std::{
io::{self, IsTerminal, Write}, io::{self, IsTerminal, Write},
path::Path, path::Path,
@ -47,6 +51,8 @@ enum Subcommands {
/// The name of the exercise /// The name of the exercise
name: Option<String>, name: Option<String>,
}, },
/// Run all the exercises, marking them as done or pending accordingly.
RunAll,
/// Reset a single exercise /// Reset a single exercise
Reset { Reset {
/// The name of the exercise /// The name of the exercise
@ -138,6 +144,36 @@ fn main() -> Result<()> {
} }
run::run(&mut app_state)?; run::run(&mut app_state)?;
} }
Some(Subcommands::RunAll) => {
let mut stdout = io::stdout().lock();
if let Some(first_fail) = app_state.check_all_exercises(&mut stdout, false)? {
let pending = app_state
.exercises()
.iter()
.filter(|exercise| !exercise.done)
.count();
if app_state.current_exercise().done {
app_state.set_current_exercise_ind(first_fail)?;
}
stdout
.queue(Print("\n"))?
.queue(SetForegroundColor(Color::Red))?
.queue(Print(format!("{pending}")))?
.queue(ResetColor)?;
if pending == 1 {
stdout.queue(Print(" exercise has some errors: "))?;
} else {
stdout.queue(Print(" exercises have errors, including "))?;
}
app_state
.current_exercise()
.terminal_file_link(&mut stdout)?;
stdout.write_all(b".\n")?;
exit(1);
} else {
app_state.render_final_message(&mut stdout)?;
}
}
Some(Subcommands::Reset { name }) => { Some(Subcommands::Reset { name }) => {
app_state.set_current_exercise_by_name(&name)?; app_state.set_current_exercise_by_name(&name)?;
let exercise_path = app_state.reset_current_exercise()?; let exercise_path = app_state.reset_current_exercise()?;

View file

@ -9,6 +9,10 @@ use std::{
io::{self, BufRead, StdoutLock, Write}, io::{self, BufRead, StdoutLock, Write},
}; };
pub const PROGRESS_FAILED_COLOR: Color = Color::Red;
pub const PROGRESS_SUCCESS_COLOR: Color = Color::Green;
pub const PROGRESS_PENDING_COLOR: Color = Color::Blue;
pub struct MaxLenWriter<'a, 'b> { pub struct MaxLenWriter<'a, 'b> {
pub stdout: &'a mut StdoutLock<'b>, pub stdout: &'a mut StdoutLock<'b>,
len: usize, len: usize,
@ -85,15 +89,26 @@ impl<'a> CountedWrite<'a> for StdoutLock<'a> {
} }
} }
/// Terminal progress bar to be used when not using Ratataui. /// Simple terminal progress bar
pub fn progress_bar<'a>( pub fn progress_bar<'a>(
writer: &mut impl CountedWrite<'a>, writer: &mut impl CountedWrite<'a>,
progress: u16, progress: u16,
total: u16, total: u16,
line_width: u16, line_width: u16,
) -> io::Result<()> {
progress_bar_with_success(writer, 0, 0, progress, total, line_width)
}
/// Terminal progress bar with three states (pending + failed + success)
pub fn progress_bar_with_success<'a>(
writer: &mut impl CountedWrite<'a>,
pending: u16,
failed: u16,
success: u16,
total: u16,
line_width: u16,
) -> io::Result<()> { ) -> io::Result<()> {
debug_assert!(total < 1000); debug_assert!(total < 1000);
debug_assert!(progress <= total); debug_assert!((pending + failed + success) <= total);
const PREFIX: &[u8] = b"Progress: ["; const PREFIX: &[u8] = b"Progress: [";
const PREFIX_WIDTH: u16 = PREFIX.len() as u16; const PREFIX_WIDTH: u16 = PREFIX.len() as u16;
@ -104,25 +119,67 @@ pub fn progress_bar<'a>(
if line_width < MIN_LINE_WIDTH { if line_width < MIN_LINE_WIDTH {
writer.write_ascii(b"Progress: ")?; writer.write_ascii(b"Progress: ")?;
// Integers are in ASCII. // Integers are in ASCII.
return writer.write_ascii(format!("{progress}/{total}").as_bytes()); return writer.write_ascii(format!("{}/{total}", failed + success).as_bytes());
} }
let stdout = writer.stdout(); let stdout = writer.stdout();
stdout.write_all(PREFIX)?; stdout.write_all(PREFIX)?;
let width = line_width - WRAPPER_WIDTH; let width = line_width - WRAPPER_WIDTH;
let filled = (width * progress) / total; let mut failed_end = (width * failed) / total;
let mut success_end = (width * (failed + success)) / total;
let mut pending_end = (width * (failed + success + pending)) / total;
stdout.queue(SetForegroundColor(Color::Green))?; // In case the range boundaries overlap, "pending" has priority over both
for _ in 0..filled { // "failed" and "success" (don't show the bar as "complete" when we are
// still checking some things).
// "Failed" has priority over "success" (don't show 100% success if we
// have some failures, at the risk of showing 100% failures even with
// a few successes).
//
// "Failed" already has priority over "success" because it's displayed
// first. But "pending" is last so we need to fix "success"/"failed".
if pending > 0 {
pending_end = pending_end.max(1);
if pending_end == success_end {
success_end -= 1;
}
if pending_end == failed_end {
failed_end -= 1;
}
// This will replace the last character of the "pending" range with
// the arrow char ('>'). This ensures that even if the progress bar
// is filled (everything either done or pending), we'll still see
// the '>' as long as we are not fully done.
pending_end -= 1;
}
if failed > 0 {
stdout.queue(SetForegroundColor(PROGRESS_FAILED_COLOR))?;
for _ in 0..failed_end {
stdout.write_all(b"#")?;
}
}
stdout.queue(SetForegroundColor(PROGRESS_SUCCESS_COLOR))?;
for _ in failed_end..success_end {
stdout.write_all(b"#")?; stdout.write_all(b"#")?;
} }
if filled < width { if pending > 0 {
stdout.queue(SetForegroundColor(PROGRESS_PENDING_COLOR))?;
for _ in success_end..pending_end {
stdout.write_all(b"#")?;
}
}
if pending_end < width {
stdout.write_all(b">")?; stdout.write_all(b">")?;
} }
let width_minus_filled = width - filled; let width_minus_filled = width - pending_end;
if width_minus_filled > 1 { if width_minus_filled > 1 {
let red_part_width = width_minus_filled - 1; let red_part_width = width_minus_filled - 1;
stdout.queue(SetForegroundColor(Color::Red))?; stdout.queue(SetForegroundColor(Color::Red))?;
@ -133,7 +190,7 @@ pub fn progress_bar<'a>(
stdout.queue(SetForegroundColor(Color::Reset))?; stdout.queue(SetForegroundColor(Color::Reset))?;
write!(stdout, "] {progress:>3}/{total}") write!(stdout, "] {:>3}/{}", failed + success, total)
} }
pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> { pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {

View file

@ -103,6 +103,13 @@ fn run_watch(
WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?, WatchEvent::Input(InputEvent::Run) => watch_state.run_current_exercise(&mut stdout)?,
WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?, WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?,
WatchEvent::Input(InputEvent::List) => return Ok(WatchExit::List), WatchEvent::Input(InputEvent::List) => return Ok(WatchExit::List),
WatchEvent::Input(InputEvent::CheckAll) => match watch_state
.check_all_exercises(&mut stdout)?
{
ExercisesProgress::AllDone => break,
ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?,
ExercisesProgress::CurrentPending => (),
},
WatchEvent::Input(InputEvent::Reset) => watch_state.reset_exercise(&mut stdout)?, WatchEvent::Input(InputEvent::Reset) => watch_state.reset_exercise(&mut stdout)?,
WatchEvent::Input(InputEvent::Quit) => { WatchEvent::Input(InputEvent::Quit) => {
stdout.write_all(QUIT_MSG)?; stdout.write_all(QUIT_MSG)?;

View file

@ -195,6 +195,11 @@ impl<'a> WatchState<'a> {
stdout.queue(ResetColor)?; stdout.queue(ResetColor)?;
stdout.write_all(b":list / ")?; stdout.write_all(b":list / ")?;
stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"c")?;
stdout.queue(ResetColor)?;
stdout.write_all(b":check all / ")?;
stdout.queue(SetAttribute(Attribute::Bold))?; stdout.queue(SetAttribute(Attribute::Bold))?;
stdout.write_all(b"x")?; stdout.write_all(b"x")?;
stdout.queue(ResetColor)?; stdout.queue(ResetColor)?;
@ -274,6 +279,23 @@ impl<'a> WatchState<'a> {
Ok(()) Ok(())
} }
pub fn check_all_exercises(&mut self, stdout: &mut StdoutLock) -> Result<ExercisesProgress> {
stdout.write_all(b"\n")?;
if let Some(first_fail) = self.app_state.check_all_exercises(stdout, false)? {
// Only change exercise if the current one is done...
if self.app_state.current_exercise().done {
self.app_state.set_current_exercise_ind(first_fail)?;
}
// ...but always pretend it's a "new" anyway because that refreshes
// the display
Ok(ExercisesProgress::NewPending)
} else {
self.app_state.render_final_message(stdout)?;
Ok(ExercisesProgress::AllDone)
}
}
pub fn update_term_width(&mut self, width: u16, stdout: &mut StdoutLock) -> io::Result<()> { pub fn update_term_width(&mut self, width: u16, stdout: &mut StdoutLock) -> io::Result<()> {
if self.term_width != width { if self.term_width != width {
self.term_width = width; self.term_width = width;

View file

@ -11,6 +11,7 @@ pub enum InputEvent {
Run, Run,
Hint, Hint,
List, List,
CheckAll,
Reset, Reset,
Quit, Quit,
} }
@ -37,6 +38,7 @@ pub fn terminal_event_handler(
KeyCode::Char('r') if manual_run => InputEvent::Run, KeyCode::Char('r') if manual_run => InputEvent::Run,
KeyCode::Char('h') => InputEvent::Hint, KeyCode::Char('h') => InputEvent::Hint,
KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List), KeyCode::Char('l') => break WatchEvent::Input(InputEvent::List),
KeyCode::Char('c') => InputEvent::CheckAll,
KeyCode::Char('x') => { KeyCode::Char('x') => {
if sender.send(WatchEvent::Input(InputEvent::Reset)).is_err() { if sender.send(WatchEvent::Input(InputEvent::Reset)).is_err() {
return; return;