diff --git a/Cargo.lock b/Cargo.lock index f083ba0..5033cc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -608,7 +608,7 @@ dependencies = [ [[package]] name = "tuitype" -version = "0.1.5" +version = "0.1.6" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 90bcbe1..873b574 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tuitype" -version = "0.1.5" +version = "0.1.6" edition = "2021" description = "A terminal-based typing test application similar to MonkeyType" authors = ["RobbyV2"] diff --git a/justfile b/justfile new file mode 100644 index 0000000..4daea4a --- /dev/null +++ b/justfile @@ -0,0 +1,88 @@ +# TuiType Project Task Runner +# Usage: just +# Run `just --list` to see all available commands + +# Default recipe - show available commands +default: + @just --list + +# Build the project in debug mode +build: + cargo build + +# Build the project in release mode +build-release: + cargo build --release + +# Run the application +run *args: + cargo run {{ args }} + +# Run tests +test: + cargo test + +# Check the project (faster than build, just checks for errors) +check: + cargo check + +# Run clippy linter +clippy: + cargo clippy + +# Format code with rustfmt +fmt: + cargo fmt + +# Check if code is formatted correctly +fmt-check: + cargo fmt -- --check + +# Clean build artifacts +clean: + cargo clean + +# Auto-fix linting issues where possible +fix: + cargo fix --allow-dirty --allow-staged + cargo clippy --fix --allow-dirty --allow-staged + +# Install the binary to ~/.cargo/bin +install: + cargo install --path . + +# Run all checks (useful for CI) +ci: fmt-check check clippy test + +# Update dependencies +update: + cargo update + +# Show cargo tree of dependencies +deps: + cargo tree + +# Build for multiple platforms (using the existing build script) +build-multi: + ./build_release.sh + +# Show project info +info: + @echo "TuiType - Terminal-based typing test application" + @echo "Version: $(grep '^version =' Cargo.toml | cut -d '"' -f 2)" + @echo "Build targets available via build-multi:" + @echo " - Linux x86_64" + @echo " - macOS x86_64" + @echo " - macOS ARM (Apple Silicon)" + @echo " - Windows x86_64" + @echo " - WebAssembly (WASI)" + @echo " - WebAssembly (Web)" + +# Development workflow - format, check, test +dev: fmt check test + +# Release workflow - all checks plus release build +release: ci build-release + +# Quick check - just build and clippy (fastest feedback) +quick: check clippy \ No newline at end of file diff --git a/src/config/mod.rs b/src/config/mod.rs index 2c63f3e..897c21d 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -130,8 +130,8 @@ pub fn theme_name(theme_type: ThemeType) -> &'static str { pub fn test_mode_name(mode: TestMode) -> String { match mode { - TestMode::Timed(seconds) => format!("{} seconds", seconds), - TestMode::Words(count) => format!("{} words", count), + TestMode::Timed(seconds) => format!("{seconds} seconds"), + TestMode::Words(count) => format!("{count} words"), TestMode::Quote => "Quote".to_string(), TestMode::Custom => "Custom".to_string(), } diff --git a/src/input/mod.rs b/src/input/mod.rs index e70f18d..b1c3b53 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -13,21 +13,13 @@ pub enum Event { } #[derive(Debug, Clone, Copy)] +#[derive(Default)] struct KeyState { last_press: Option, last_release: Option, is_held: bool, } -impl Default for KeyState { - fn default() -> Self { - Self { - last_press: None, - last_release: None, - is_held: false, - } - } -} impl KeyState { fn should_process_key(&mut self, now: Instant, kind: KeyEventKind) -> bool { diff --git a/src/main.rs b/src/main.rs index e3e27b7..4057202 100644 --- a/src/main.rs +++ b/src/main.rs @@ -36,7 +36,7 @@ fn main() -> Result<()> { execute!(backend, LeaveAlternateScreen, DisableMouseCapture)?; if let Err(err) = res { - println!("Error: {:?}", err) + println!("Error: {err:?}") } Ok(()) diff --git a/src/ui/draw.rs b/src/ui/draw.rs index be901b6..5097a34 100644 --- a/src/ui/draw.rs +++ b/src/ui/draw.rs @@ -68,8 +68,8 @@ fn draw_typing_area(app: &App, frame: &mut Frame, area: Rect) { ); let test_mode_str = match app.config.test_mode { - crate::config::TestMode::Timed(secs) => format!("Mode: Timed {}s", secs), - crate::config::TestMode::Words(count) => format!("Mode: Words {}", count), + crate::config::TestMode::Timed(secs) => format!("Mode: Timed {secs}s"), + crate::config::TestMode::Words(count) => format!("Mode: Words {count}"), crate::config::TestMode::Quote => "Mode: Quote".to_string(), crate::config::TestMode::Custom => "Mode: Custom".to_string(), }; @@ -97,12 +97,12 @@ fn draw_typing_area(app: &App, frame: &mut Frame, area: Rect) { let time_remaining_str = if let Some(remaining) = app.time_remaining { match app.config.test_mode { - crate::config::TestMode::Timed(_) => format!("Time: {}s", remaining), + crate::config::TestMode::Timed(_) => format!("Time: {remaining}s"), _ => String::new(), } } else { match app.config.test_mode { - crate::config::TestMode::Timed(secs) => format!("Time: {}s", secs), + crate::config::TestMode::Timed(secs) => format!("Time: {secs}s"), _ => String::new(), } }; @@ -114,19 +114,11 @@ fn draw_typing_area(app: &App, frame: &mut Frame, area: Rect) { let single_line = if !time_remaining_str.is_empty() { format!( - "{} | {} | {} | {} | {} | {} | {} | Press ESC for menu", - app_title, - test_mode_str, - diff_str, - repeat_mode_str, - end_on_error_str, - stats_str, - time_remaining_str + "{app_title} | {test_mode_str} | {diff_str} | {repeat_mode_str} | {end_on_error_str} | {stats_str} | {time_remaining_str} | Press ESC for menu" ) } else { format!( - "{} | {} | {} | {} | {} | {} | Press ESC for menu", - app_title, test_mode_str, diff_str, repeat_mode_str, end_on_error_str, stats_str + "{app_title} | {test_mode_str} | {diff_str} | {repeat_mode_str} | {end_on_error_str} | {stats_str} | Press ESC for menu" ) }; @@ -156,10 +148,10 @@ fn draw_typing_area(app: &App, frame: &mut Frame, area: Rect) { let show_time = !time_remaining_str.is_empty(); if show_time { - first_line = format!("{} | {}", app_title, time_remaining_str); + first_line = format!("{app_title} | {time_remaining_str}"); second_line = format!("WPM: {:.1} | {}", app.stats.wpm, esc_menu_text); } else { - first_line = format!("{}", app_title); + first_line = app_title.to_string(); second_line = format!("WPM: {:.1} | {}", app.stats.wpm, esc_menu_text); } } else if area.width < 60 { @@ -167,9 +159,9 @@ fn draw_typing_area(app: &App, frame: &mut Frame, area: Rect) { let show_time = !time_remaining_str.is_empty(); if show_time { - first_line = format!("{} | {}", app_title, time_remaining_str); + first_line = format!("{app_title} | {time_remaining_str}"); } else { - first_line = format!("{} | {}", app_title, test_mode_str); + first_line = format!("{app_title} | {test_mode_str}"); } second_line = format!( @@ -178,39 +170,37 @@ fn draw_typing_area(app: &App, frame: &mut Frame, area: Rect) { ); } else { let first_row_with_config = if area.width <= 90 { - format!("{} | {}", app_title, test_mode_str) + format!("{app_title} | {test_mode_str}") } else { format!( - "{} | {} | {} | {} | {}", - app_title, test_mode_str, diff_str, repeat_mode_str, end_on_error_str + "{app_title} | {test_mode_str} | {diff_str} | {repeat_mode_str} | {end_on_error_str}" ) }; let first_row_with_time = if !time_remaining_str.is_empty() { - format!("{} | {}", first_row_with_config, time_remaining_str) + format!("{first_row_with_config} | {time_remaining_str}") } else { first_row_with_config.clone() }; if area.width <= 90 { - first_line = format!("{} | {}", first_row_with_time, stats_str); + first_line = format!("{first_row_with_time} | {stats_str}"); second_line = format!( - "{} | {} | {} | Press ESC for menu", - diff_str, repeat_mode_str, end_on_error_str + "{diff_str} | {repeat_mode_str} | {end_on_error_str} | Press ESC for menu" ); } else if first_row_with_time.chars().count() + stats_str.chars().count() + 3 <= width_available { - first_line = format!("{} | {}", first_row_with_time, stats_str); - second_line = format!("Press ESC for menu"); + first_line = format!("{first_row_with_time} | {stats_str}"); + second_line = "Press ESC for menu".to_string(); } else if first_row_with_config.chars().count() + time_remaining_str.chars().count() + 3 <= width_available { first_line = first_row_with_time; - second_line = format!("{} | Press ESC for menu", stats_str); + second_line = format!("{stats_str} | Press ESC for menu"); } else { first_line = first_row_with_config; - second_line = format!("{} | Press ESC for menu", stats_str); + second_line = format!("{stats_str} | Press ESC for menu"); } } @@ -242,7 +232,7 @@ fn draw_typing_area(app: &App, frame: &mut Frame, area: Rect) { frame.render_widget(settings_block, line2_area); if area.width < 80 && inner_area.height > 2 && area.width >= 50 { - let third_line = format!("{}", stats_str); + let third_line = stats_str.to_string(); let stats_block = Block::default() .title(third_line) @@ -474,7 +464,7 @@ fn draw_test_complete_new(app: &App, frame: &mut Frame, area: Rect) { let block = Block::default() .borders(Borders::ALL) - .title(format!(" {} - TEST COMPLETE ", app_title)) + .title(format!(" {app_title} - TEST COMPLETE ")) .title_style( Style::default() .fg(Color::White) @@ -545,7 +535,7 @@ fn draw_test_complete_new(app: &App, frame: &mut Frame, area: Rect) { Line::from(vec![ Span::raw("Time: "), Span::styled( - format!("{:.1}s", duration), + format!("{duration:.1}s"), Style::default().add_modifier(Modifier::BOLD), ), ]), @@ -564,8 +554,8 @@ fn draw_test_complete_new(app: &App, frame: &mut Frame, area: Rect) { } let test_mode_str = match app.config.test_mode { - crate::config::TestMode::Timed(secs) => format!("Timed - {}s", secs), - crate::config::TestMode::Words(count) => format!("Words - {}", count), + crate::config::TestMode::Timed(secs) => format!("Timed - {secs}s"), + crate::config::TestMode::Words(count) => format!("Words - {count}"), crate::config::TestMode::Quote => "Quote".to_string(), crate::config::TestMode::Custom => "Custom".to_string(), }; @@ -884,7 +874,7 @@ fn draw_gauges(app: &App, frame: &mut Frame, area: Rect) { let progress_value = progress.min(100); - let progress_label = format!("Progress: {}%", progress_value); + let progress_label = format!("Progress: {progress_value}%"); let progress_gauge = Gauge::default() .block(Block::default().borders(Borders::ALL).title("Progress")) .gauge_style(Style::default().fg(Color::Rgb( @@ -901,7 +891,7 @@ fn draw_chart(app: &App, frame: &mut Frame, area: Rect) { if area.width < 20 || area.height < 4 { if !app.stats.wpm_samples.is_empty() { let latest_wpm = app.stats.wpm_samples.last().unwrap_or(&0.0); - let placeholder = format!("WPM: {:.1}", latest_wpm); + let placeholder = format!("WPM: {latest_wpm:.1}"); let placeholder_widget = Paragraph::new(placeholder) .block(Block::default().borders(Borders::ALL).title("Current WPM")) .alignment(Alignment::Center); @@ -1004,7 +994,7 @@ fn draw_chart(app: &App, frame: &mut Frame, area: Rect) { .labels(vec![ Span::raw("0"), Span::raw(format!("{:.0}", max_wpm / 2.0)), - Span::raw(format!("{:.0}", max_wpm)), + Span::raw(format!("{max_wpm:.0}")), ]), ); @@ -1036,7 +1026,7 @@ fn draw_menu(app: &App, frame: &mut Frame, area: Rect) { _ => "Menu", }; - let text = format!("{}\nPress ESC to return", menu_type); + let text = format!("{menu_type}\nPress ESC to return"); let paragraph = Paragraph::new(text) .alignment(Alignment::Center) .style(Style::default().fg(Color::White)); @@ -1063,7 +1053,7 @@ fn draw_menu(app: &App, frame: &mut Frame, area: Rect) { _ => "", }; - let title = format!(" {} - {} ", app_title, menu_type); + let title = format!(" {app_title} - {menu_type} "); let outline = Block::default() .borders(Borders::ALL) @@ -1094,7 +1084,7 @@ fn draw_menu(app: &App, frame: &mut Frame, area: Rect) { for (item, selected) in items { if selected { text.push(Line::from(vec![Span::styled( - format!("> {} <", item), + format!("> {item} <"), Style::default().add_modifier(Modifier::REVERSED), )])); } else { @@ -1117,7 +1107,7 @@ fn draw_menu(app: &App, frame: &mut Frame, area: Rect) { .map(|(item, selected)| { if *selected { Line::from(vec![Span::styled( - format!("> {} <", item), + format!("> {item} <"), Style::default().add_modifier(Modifier::REVERSED), )]) } else { @@ -1139,7 +1129,7 @@ fn draw_menu(app: &App, frame: &mut Frame, area: Rect) { .map(|(item, selected)| { if *selected { Line::from(vec![Span::styled( - format!("> {} <", item), + format!("> {item} <"), Style::default().add_modifier(Modifier::REVERSED), )]) } else { @@ -1163,7 +1153,7 @@ fn draw_menu(app: &App, frame: &mut Frame, area: Rect) { .map(|(item, selected)| { if *selected { Line::from(vec![Span::styled( - format!("> {} <", item), + format!("> {item} <"), Style::default().add_modifier(Modifier::REVERSED), )]) } else { @@ -1186,7 +1176,7 @@ fn draw_menu(app: &App, frame: &mut Frame, area: Rect) { .map(|(item, selected)| { if *selected { Line::from(vec![Span::styled( - format!("> {} <", item), + format!("> {item} <"), Style::default().add_modifier(Modifier::REVERSED), )]) } else { @@ -1210,7 +1200,7 @@ fn draw_menu(app: &App, frame: &mut Frame, area: Rect) { .map(|(item, selected)| { if *selected { Line::from(vec![Span::styled( - format!("> {} <", item), + format!("> {item} <"), Style::default().add_modifier(Modifier::REVERSED), )]) } else { @@ -1283,7 +1273,7 @@ fn draw_menu(app: &App, frame: &mut Frame, area: Rect) { )] } else { vec![Span::styled( - format!("{} ▋", input), + format!("{input} ▋"), Style::default().add_modifier(Modifier::BOLD), )] }), @@ -1306,7 +1296,7 @@ fn draw_menu(app: &App, frame: &mut Frame, area: Rect) { )] } else { vec![Span::styled( - format!("{} ▋", input), + format!("{input} ▋"), Style::default().add_modifier(Modifier::BOLD), )] }), @@ -1391,7 +1381,7 @@ fn draw_menu(app: &App, frame: &mut Frame, area: Rect) { Line::from(format!("WPM: {:.1}", app.stats.wpm)), Line::from(format!("Raw WPM: {:.1}", app.stats.raw_wpm)), Line::from(format!("Accuracy: {:.1}%", app.stats.accuracy)), - Line::from(format!("Time: {:.1} seconds", duration)), + Line::from(format!("Time: {duration:.1} seconds")), ]; if let Some(reason) = &app.test_end_reason { @@ -1485,7 +1475,7 @@ fn draw_warning(app: &App, frame: &mut Frame, area: Rect) { let block = Block::default() .borders(Borders::ALL) .border_style(Style::default().fg(Color::Red)) - .title(format!(" {} - REPEAT MODE WARNING ", app_title)) + .title(format!(" {app_title} - REPEAT MODE WARNING ")) .title_style( Style::default() .fg(Color::White) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 93e436f..fc6a134 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -207,15 +207,14 @@ impl App { _ => 60, }; - if !matches!(self.config.test_mode, TestMode::Timed(_)) { - if !self.can_change_settings("test_mode") { + if !matches!(self.config.test_mode, TestMode::Timed(_)) + && !self.can_change_settings("test_mode") { self.set_repeat_mode_warning( "Test mode cannot be changed while Repeat Mode is active." .to_string(), ); return Ok(()); } - } self.config.test_mode = TestMode::Timed(seconds); self.time_remaining = Some(seconds); @@ -253,14 +252,12 @@ impl App { ); return Ok(()); } - } else { - if !self.can_change_settings("test_mode") { - self.set_repeat_mode_warning( - "Word count cannot be changed while Repeat Mode is active." - .to_string(), - ); - return Ok(()); - } + } else if !self.can_change_settings("test_mode") { + self.set_repeat_mode_warning( + "Word count cannot be changed while Repeat Mode is active." + .to_string(), + ); + return Ok(()); } let words = match idx { @@ -535,11 +532,10 @@ impl App { pub fn handle_key_event(&mut self, key_event: crossterm::event::KeyEvent) -> AppResult<()> { use crossterm::event::{KeyCode, KeyModifiers}; - if self.warning_state != WarningState::None { - if self.handle_warning(key_event) { + if self.warning_state != WarningState::None + && self.handle_warning(key_event) { return Ok(()); } - } let now = Instant::now(); let key_code = key_event.code; @@ -598,12 +594,10 @@ impl App { if self.test_complete { self.restart_test(); self.menu_state = MenuState::Typing; + } else if self.menu_state == MenuState::Typing { + self.menu_state = MenuState::MainMenu(0); } else { - if self.menu_state == MenuState::Typing { - self.menu_state = MenuState::MainMenu(0); - } else { - self.menu_state = MenuState::Typing; - } + self.menu_state = MenuState::Typing; } } @@ -813,18 +807,16 @@ impl App { } } } - } else { - if !matches!(self.config.test_mode, TestMode::Timed(_)) { - let is_word_limit_reached = if self.text_source.is_scrollable { - self.text_source.is_complete() - && self.typed_text.len() >= self.text_source.full_text().len() - } else { - self.typed_text.len() >= self.text_source.full_text().len() - }; + } else if !matches!(self.config.test_mode, TestMode::Timed(_)) { + let is_word_limit_reached = if self.text_source.is_scrollable { + self.text_source.is_complete() + && self.typed_text.len() >= self.text_source.full_text().len() + } else { + self.typed_text.len() >= self.text_source.full_text().len() + }; - if is_word_limit_reached { - self.complete_test(); - } + if is_word_limit_reached { + self.complete_test(); } } diff --git a/src/util/mod.rs b/src/util/mod.rs index cfe22be..b2fcfce 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -5,7 +5,7 @@ pub fn format_duration(duration: Duration) -> String { let minutes = total_seconds / 60; let seconds = total_seconds % 60; - format!("{:02}:{:02}", minutes, seconds) + format!("{minutes:02}:{seconds:02}") } pub fn format_elapsed(start_time: std::time::Instant) -> String { @@ -13,7 +13,7 @@ pub fn format_elapsed(start_time: std::time::Instant) -> String { } pub fn format_wpm(wpm: f64) -> String { - format!("{:.1}", wpm) + format!("{wpm:.1}") } pub fn calculate_percentage(part: usize, total: usize) -> f64 {