Compare commits

...

8 commits

Author SHA1 Message Date
mo8it 9faa5d3aa4 Avoid asking for terminal size on each rendering 2024-09-05 17:45:27 +02:00
mo8it bcc2a136c8 Add error message when unable to get terminal size 2024-09-05 17:37:34 +02:00
mo8it dcad002057 Only render when needed 2024-09-05 17:32:59 +02:00
mo8it 51b8d2ab25 Remove unused import 2024-09-05 17:23:56 +02:00
mo8it aa3eda70e5 Simplify handling terminal events for unbuffered stdin 2024-09-05 17:12:26 +02:00
mo8it 2d0860fe1b Hide input and disable its line buffering 2024-09-05 02:11:19 +02:00
mo8it 17877366b7 Update deps 2024-09-05 01:55:31 +02:00
mo8it 5eb3dee59c Create solution even if the solution's directory is missing 2024-09-05 00:21:24 +02:00
11 changed files with 158 additions and 133 deletions

21
Cargo.lock generated
View file

@ -95,9 +95,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.16" version = "4.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@ -105,9 +105,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.15" version = "4.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73"
dependencies = [ dependencies = [
"anstream", "anstream",
"anstyle", "anstyle",
@ -242,9 +242,9 @@ checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.4.0" version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown", "hashbrown",
@ -490,6 +490,7 @@ dependencies = [
"crossterm", "crossterm",
"notify-debouncer-mini", "notify-debouncer-mini",
"os_pipe", "os_pipe",
"rustix",
"rustlings-macros", "rustlings-macros",
"serde", "serde",
"serde_json", "serde_json",
@ -549,9 +550,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.127" version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@ -612,9 +613,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.76" version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",

View file

@ -48,15 +48,18 @@ include = [
[dependencies] [dependencies]
ahash = { version = "0.8.11", default-features = false } ahash = { version = "0.8.11", default-features = false }
anyhow = "1.0.86" anyhow = "1.0.86"
clap = { version = "4.5.16", features = ["derive"] } clap = { version = "4.5.17", features = ["derive"] }
crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] } crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] }
notify-debouncer-mini = { version = "0.4.1", default-features = false } notify-debouncer-mini = { version = "0.4.1", default-features = false }
os_pipe = "1.2.1" os_pipe = "1.2.1"
rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" } rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" }
serde_json = "1.0.127" serde_json = "1.0.128"
serde.workspace = true serde.workspace = true
toml_edit.workspace = true toml_edit.workspace = true
[target.'cfg(not(windows))'.dependencies]
rustix = { version = "0.38.35", default-features = false, features = ["std", "stdio", "termios"] }
[dev-dependencies] [dev-dependencies]
tempfile = "3.12.0" tempfile = "3.12.0"

View file

@ -24,10 +24,10 @@ const STATE_FILE_NAME: &str = ".rustlings-state.txt";
pub enum ExercisesProgress { pub enum ExercisesProgress {
// All exercises are done. // All exercises are done.
AllDone, AllDone,
// The current exercise failed and is still pending.
CurrentPending,
// A new exercise is now pending. // A new exercise is now pending.
NewPending, NewPending,
// The current exercise is still pending.
CurrentPending,
} }
pub enum StateFileStatus { pub enum StateFileStatus {

View file

@ -1,7 +1,7 @@
use anyhow::{Context, Error, Result}; use anyhow::{Context, Error, Result};
use std::{ use std::{
fs::{create_dir, OpenOptions}, fs::{self, create_dir},
io::{self, Write}, io,
}; };
use crate::info_file::ExerciseInfo; use crate::info_file::ExerciseInfo;
@ -9,29 +9,6 @@ use crate::info_file::ExerciseInfo;
/// Contains all embedded files. /// Contains all embedded files.
pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!();
#[derive(Clone, Copy)]
pub enum WriteStrategy {
IfNotExists,
Overwrite,
}
impl WriteStrategy {
fn write(self, path: &str, content: &[u8]) -> Result<()> {
let file = match self {
Self::IfNotExists => OpenOptions::new().create_new(true).write(true).open(path),
Self::Overwrite => OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path),
};
file.with_context(|| format!("Failed to open the file `{path}` in write mode"))?
.write_all(content)
.with_context(|| format!("Failed to write the file {path}"))
}
}
// Files related to one exercise. // Files related to one exercise.
struct ExerciseFiles { struct ExerciseFiles {
// The content of the exercise file. // The content of the exercise file.
@ -42,6 +19,16 @@ struct ExerciseFiles {
dir_ind: usize, dir_ind: usize,
} }
fn create_dir_if_not_exists(path: &str) -> Result<()> {
if let Err(e) = create_dir(path) {
if e.kind() != io::ErrorKind::AlreadyExists {
return Err(Error::from(e).context(format!("Failed to create the directory {path}")));
}
}
Ok(())
}
// A directory in the `exercises/` directory. // A directory in the `exercises/` directory.
pub struct ExerciseDir { pub struct ExerciseDir {
pub name: &'static str, pub name: &'static str,
@ -55,21 +42,13 @@ impl ExerciseDir {
let mut dir_path = String::with_capacity(20 + self.name.len()); let mut dir_path = String::with_capacity(20 + self.name.len());
dir_path.push_str("exercises/"); dir_path.push_str("exercises/");
dir_path.push_str(self.name); dir_path.push_str(self.name);
create_dir_if_not_exists(&dir_path)?;
if let Err(e) = create_dir(&dir_path) {
if e.kind() == io::ErrorKind::AlreadyExists {
return Ok(());
}
return Err(
Error::from(e).context(format!("Failed to create the directory {dir_path}"))
);
}
let mut readme_path = dir_path; let mut readme_path = dir_path;
readme_path.push_str("/README.md"); readme_path.push_str("/README.md");
WriteStrategy::Overwrite.write(&readme_path, self.readme) fs::write(&readme_path, self.readme)
.with_context(|| format!("Failed to write the file {readme_path}"))
} }
} }
@ -86,17 +65,31 @@ impl EmbeddedFiles {
pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> { pub fn init_exercises_dir(&self, exercise_infos: &[ExerciseInfo]) -> Result<()> {
create_dir("exercises").context("Failed to create the directory `exercises`")?; create_dir("exercises").context("Failed to create the directory `exercises`")?;
WriteStrategy::IfNotExists.write( fs::write(
"exercises/README.md", "exercises/README.md",
include_bytes!("../exercises/README.md"), include_bytes!("../exercises/README.md"),
)?; )
.context("Failed to write the file exercises/README.md")?;
for dir in self.exercise_dirs { for dir in self.exercise_dirs {
dir.init_on_disk()?; dir.init_on_disk()?;
} }
let mut exercise_path = String::with_capacity(64);
let prefix = "exercises/";
exercise_path.push_str(prefix);
for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) { for (exercise_info, exercise_files) in exercise_infos.iter().zip(self.exercise_files) {
WriteStrategy::IfNotExists.write(&exercise_info.path(), exercise_files.exercise)?; let dir = &self.exercise_dirs[exercise_files.dir_ind];
exercise_path.truncate(prefix.len());
exercise_path.push_str(dir.name);
exercise_path.push('/');
exercise_path.push_str(&exercise_info.name);
exercise_path.push_str(".rs");
fs::write(&exercise_path, exercise_files.exercise)
.with_context(|| format!("Failed to write the exercise file {exercise_path}"))?;
} }
Ok(()) Ok(())
@ -107,7 +100,8 @@ impl EmbeddedFiles {
let dir = &self.exercise_dirs[exercise_files.dir_ind]; let dir = &self.exercise_dirs[exercise_files.dir_ind];
dir.init_on_disk()?; dir.init_on_disk()?;
WriteStrategy::Overwrite.write(path, exercise_files.exercise) fs::write(path, exercise_files.exercise)
.with_context(|| format!("Failed to write the exercise file {path}"))
} }
/// Write the solution file to disk and return its path. /// Write the solution file to disk and return its path.
@ -116,19 +110,25 @@ impl EmbeddedFiles {
exercise_ind: usize, exercise_ind: usize,
exercise_name: &str, exercise_name: &str,
) -> Result<String> { ) -> Result<String> {
create_dir_if_not_exists("solutions")?;
let exercise_files = &self.exercise_files[exercise_ind]; let exercise_files = &self.exercise_files[exercise_ind];
let dir = &self.exercise_dirs[exercise_files.dir_ind]; let dir = &self.exercise_dirs[exercise_files.dir_ind];
// 14 = 10 + 1 + 3 // 14 = 10 + 1 + 3
// solutions/ + / + .rs // solutions/ + / + .rs
let mut solution_path = String::with_capacity(14 + dir.name.len() + exercise_name.len()); let mut dir_path = String::with_capacity(14 + dir.name.len() + exercise_name.len());
solution_path.push_str("solutions/"); dir_path.push_str("solutions/");
solution_path.push_str(dir.name); dir_path.push_str(dir.name);
create_dir_if_not_exists(&dir_path)?;
let mut solution_path = dir_path;
solution_path.push('/'); solution_path.push('/');
solution_path.push_str(exercise_name); solution_path.push_str(exercise_name);
solution_path.push_str(".rs"); solution_path.push_str(".rs");
WriteStrategy::Overwrite.write(&solution_path, exercise_files.solution)?; fs::write(&solution_path, exercise_files.solution)
.with_context(|| format!("Failed to write the solution file {solution_path}"))?;
Ok(solution_path) Ok(solution_path)
} }

View file

@ -20,7 +20,7 @@ mod scroll_state;
mod state; mod state;
fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> { fn handle_list(app_state: &mut AppState, stdout: &mut StdoutLock) -> Result<()> {
let mut list_state = ListState::new(app_state, stdout)?; let mut list_state = ListState::build(app_state, stdout)?;
let mut is_searching = false; let mut is_searching = false;
loop { loop {

View file

@ -48,7 +48,7 @@ pub struct ListState<'a> {
} }
impl<'a> ListState<'a> { impl<'a> ListState<'a> {
pub fn new(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> io::Result<Self> { pub fn build(app_state: &'a mut AppState, stdout: &mut StdoutLock) -> Result<Self> {
stdout.queue(Clear(ClearType::All))?; stdout.queue(Clear(ClearType::All))?;
let name_col_title_len = 4; let name_col_title_len = 4;
@ -64,7 +64,7 @@ impl<'a> ListState<'a> {
let n_rows_with_filter = app_state.exercises().len(); let n_rows_with_filter = app_state.exercises().len();
let selected = app_state.current_exercise_ind(); let selected = app_state.current_exercise_ind();
let (width, height) = terminal::size()?; let (width, height) = terminal::size().context("Failed to get the terminal size")?;
let scroll_state = ScrollState::new(n_rows_with_filter, Some(selected), 5); let scroll_state = ScrollState::new(n_rows_with_filter, Some(selected), 5);
let mut slf = Self { let mut slf = Self {

View file

@ -8,7 +8,7 @@ use std::{
}; };
use term::{clear_terminal, press_enter_prompt}; use term::{clear_terminal, press_enter_prompt};
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile};
mod app_state; mod app_state;
mod cargo_toml; mod cargo_toml;
@ -130,15 +130,7 @@ fn main() -> Result<()> {
) )
}; };
loop { watch::watch(&mut app_state, notify_exercise_names)?;
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 }) => { Some(Subcommands::Run { name }) => {
if let Some(name) = name { if let Some(name) = name {

View file

@ -45,7 +45,7 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
} }
match app_state.done_current_exercise(&mut stdout)? { match app_state.done_current_exercise(&mut stdout)? {
ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => { ExercisesProgress::NewPending | ExercisesProgress::CurrentPending => {
stdout.write_all(b"Next exercise: ")?; stdout.write_all(b"Next exercise: ")?;
app_state app_state
.current_exercise() .current_exercise()

View file

@ -11,7 +11,10 @@ use std::{
time::Duration, time::Duration,
}; };
use crate::app_state::{AppState, ExercisesProgress}; use crate::{
app_state::{AppState, ExercisesProgress},
list,
};
use self::{ use self::{
notify_event::NotifyEventHandler, notify_event::NotifyEventHandler,
@ -26,22 +29,21 @@ mod terminal_event;
enum WatchEvent { enum WatchEvent {
Input(InputEvent), Input(InputEvent),
FileChange { exercise_ind: usize }, FileChange { exercise_ind: usize },
TerminalResize, TerminalResize { width: u16 },
NotifyErr(notify::Error), NotifyErr(notify::Error),
TerminalEventErr(io::Error), TerminalEventErr(io::Error),
} }
/// Returned by the watch mode to indicate what to do afterwards. /// Returned by the watch mode to indicate what to do afterwards.
#[must_use] #[must_use]
pub enum WatchExit { enum WatchExit {
/// Exit the program. /// Exit the program.
Shutdown, Shutdown,
/// Enter the list mode and restart the watch mode afterwards. /// Enter the list mode and restart the watch mode afterwards.
List, List,
} }
/// `notify_exercise_names` as None activates the manual run mode. fn run_watch(
pub fn watch(
app_state: &mut AppState, app_state: &mut AppState,
notify_exercise_names: Option<&'static [&'static [u8]]>, notify_exercise_names: Option<&'static [&'static [u8]]>,
) -> Result<WatchExit> { ) -> Result<WatchExit> {
@ -70,7 +72,7 @@ pub fn watch(
None None
}; };
let mut watch_state = WatchState::new(app_state, manual_run); let mut watch_state = WatchState::build(app_state, manual_run)?;
let mut stdout = io::stdout().lock(); let mut stdout = io::stdout().lock();
watch_state.run_current_exercise(&mut stdout)?; watch_state.run_current_exercise(&mut stdout)?;
@ -81,26 +83,23 @@ pub fn watch(
match event { match event {
WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise(&mut stdout)? { WatchEvent::Input(InputEvent::Next) => match watch_state.next_exercise(&mut stdout)? {
ExercisesProgress::AllDone => break, ExercisesProgress::AllDone => break,
ExercisesProgress::CurrentPending => watch_state.render(&mut stdout)?,
ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?, ExercisesProgress::NewPending => watch_state.run_current_exercise(&mut stdout)?,
ExercisesProgress::CurrentPending => (),
}, },
WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?, WatchEvent::Input(InputEvent::Hint) => watch_state.show_hint(&mut stdout)?,
WatchEvent::Input(InputEvent::List) => { WatchEvent::Input(InputEvent::List) => return Ok(WatchExit::List),
return Ok(WatchExit::List);
}
WatchEvent::Input(InputEvent::Quit) => { WatchEvent::Input(InputEvent::Quit) => {
stdout.write_all(QUIT_MSG)?; stdout.write_all(QUIT_MSG)?;
break; break;
} }
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::Unrecognized) => watch_state.render(&mut stdout)?,
WatchEvent::FileChange { exercise_ind } => { WatchEvent::FileChange { exercise_ind } => {
watch_state.handle_file_change(exercise_ind, &mut stdout)?; watch_state.handle_file_change(exercise_ind, &mut stdout)?;
} }
WatchEvent::TerminalResize => watch_state.render(&mut stdout)?, WatchEvent::TerminalResize { width } => {
WatchEvent::NotifyErr(e) => { watch_state.update_term_width(width, &mut stdout)?;
return Err(Error::from(e).context(NOTIFY_ERR));
} }
WatchEvent::NotifyErr(e) => return Err(Error::from(e).context(NOTIFY_ERR)),
WatchEvent::TerminalEventErr(e) => { WatchEvent::TerminalEventErr(e) => {
return Err(Error::from(e).context("Terminal event listener failed")); return Err(Error::from(e).context("Terminal event listener failed"));
} }
@ -110,6 +109,48 @@ pub fn watch(
Ok(WatchExit::Shutdown) Ok(WatchExit::Shutdown)
} }
fn watch_list_loop(
app_state: &mut AppState,
notify_exercise_names: Option<&'static [&'static [u8]]>,
) -> Result<()> {
loop {
match run_watch(app_state, notify_exercise_names)? {
WatchExit::Shutdown => break Ok(()),
// 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(app_state)?,
}
}
}
/// `notify_exercise_names` as None activates the manual run mode.
pub fn watch(
app_state: &mut AppState,
notify_exercise_names: Option<&'static [&'static [u8]]>,
) -> Result<()> {
#[cfg(not(windows))]
{
let stdin_fd = rustix::stdio::stdin();
let mut termios = rustix::termios::tcgetattr(stdin_fd)?;
let original_local_modes = termios.local_modes;
// Disable stdin line buffering and hide input.
termios.local_modes -=
rustix::termios::LocalModes::ICANON | rustix::termios::LocalModes::ECHO;
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?;
let res = watch_list_loop(app_state, notify_exercise_names);
termios.local_modes = original_local_modes;
rustix::termios::tcsetattr(stdin_fd, rustix::termios::OptionalActions::Now, &termios)?;
res
}
#[cfg(windows)]
watch_list_loop(app_state, notify_exercise_names)
}
const QUIT_MSG: &[u8] = b" const QUIT_MSG: &[u8] = b"
We hope you're enjoying learning Rust! We hope you're enjoying learning Rust!
If you want to continue working on the exercises at a later point, you can simply run `rustlings` again. If you want to continue working on the exercises at a later point, you can simply run `rustlings` again.

View file

@ -1,4 +1,4 @@
use anyhow::Result; use anyhow::{Context, Result};
use crossterm::{ use crossterm::{
style::{ style::{
Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor, Attribute, Attributes, Color, ResetColor, SetAttribute, SetAttributes, SetForegroundColor,
@ -27,17 +27,23 @@ pub struct WatchState<'a> {
show_hint: bool, show_hint: bool,
done_status: DoneStatus, done_status: DoneStatus,
manual_run: bool, manual_run: bool,
term_width: u16,
} }
impl<'a> WatchState<'a> { impl<'a> WatchState<'a> {
pub fn new(app_state: &'a mut AppState, manual_run: bool) -> Self { pub fn build(app_state: &'a mut AppState, manual_run: bool) -> Result<Self> {
Self { let term_width = terminal::size()
.context("Failed to get the terminal size")?
.0;
Ok(Self {
app_state, app_state,
output: Vec::with_capacity(OUTPUT_CAPACITY), output: Vec::with_capacity(OUTPUT_CAPACITY),
show_hint: false, show_hint: false,
done_status: DoneStatus::Pending, done_status: DoneStatus::Pending,
manual_run, manual_run,
} term_width,
})
} }
pub fn run_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> { pub fn run_current_exercise(&mut self, stdout: &mut StdoutLock) -> Result<()> {
@ -175,12 +181,11 @@ impl<'a> WatchState<'a> {
)?; )?;
} }
let line_width = terminal::size()?.0;
progress_bar( progress_bar(
stdout, stdout,
self.app_state.n_done(), self.app_state.n_done(),
self.app_state.exercises().len() as u16, self.app_state.exercises().len() as u16,
line_width, self.term_width,
)?; )?;
stdout.write_all(b"\nCurrent exercise: ")?; stdout.write_all(b"\nCurrent exercise: ")?;
@ -195,7 +200,20 @@ impl<'a> WatchState<'a> {
} }
pub fn show_hint(&mut self, stdout: &mut StdoutLock) -> io::Result<()> { pub fn show_hint(&mut self, stdout: &mut StdoutLock) -> io::Result<()> {
self.show_hint = true; if !self.show_hint {
self.render(stdout) self.show_hint = true;
self.render(stdout)?;
}
Ok(())
}
pub fn update_term_width(&mut self, width: u16, stdout: &mut StdoutLock) -> io::Result<()> {
if self.term_width != width {
self.term_width = width;
self.render(stdout)?;
}
Ok(())
} }
} }

View file

@ -1,4 +1,4 @@
use crossterm::event::{self, Event, KeyCode, KeyEventKind, KeyModifiers}; use crossterm::event::{self, Event, KeyCode, KeyEventKind};
use std::sync::mpsc::Sender; use std::sync::mpsc::Sender;
use super::WatchEvent; use super::WatchEvent;
@ -9,13 +9,9 @@ pub enum InputEvent {
Hint, Hint,
List, List,
Quit, Quit,
Unrecognized,
} }
pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) { pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
// Only send `Unrecognized` on ENTER if the last input wasn't valid.
let mut last_input_valid = false;
let last_input_event = loop { let last_input_event = loop {
let terminal_event = match event::read() { let terminal_event = match event::read() {
Ok(v) => v, Ok(v) => v,
@ -34,47 +30,21 @@ pub fn terminal_event_handler(tx: Sender<WatchEvent>, manual_run: bool) {
KeyEventKind::Press => (), KeyEventKind::Press => (),
} }
if key.modifiers != KeyModifiers::NONE {
last_input_valid = false;
continue;
}
let input_event = match key.code { let input_event = match key.code {
KeyCode::Enter => { KeyCode::Char('n') => InputEvent::Next,
if last_input_valid { KeyCode::Char('h') => InputEvent::Hint,
continue; KeyCode::Char('l') => break InputEvent::List,
} KeyCode::Char('q') => break InputEvent::Quit,
KeyCode::Char('r') if manual_run => InputEvent::Run,
InputEvent::Unrecognized _ => continue,
}
KeyCode::Char(c) => {
let input_event = match c {
'n' => InputEvent::Next,
'h' => InputEvent::Hint,
'l' => break InputEvent::List,
'q' => break InputEvent::Quit,
'r' if manual_run => InputEvent::Run,
_ => {
last_input_valid = false;
continue;
}
};
last_input_valid = true;
input_event
}
_ => {
last_input_valid = false;
continue;
}
}; };
if tx.send(WatchEvent::Input(input_event)).is_err() { if tx.send(WatchEvent::Input(input_event)).is_err() {
return; return;
} }
} }
Event::Resize(_, _) => { Event::Resize(width, _) => {
if tx.send(WatchEvent::TerminalResize).is_err() { if tx.send(WatchEvent::TerminalResize { width }).is_err() {
return; return;
} }
} }