practicode
Advanced tools
+1
-1
@@ -456,3 +456,3 @@ # This file is automatically @generated by Cargo. | ||
| name = "practicode" | ||
| version = "0.1.18" | ||
| version = "0.1.19" | ||
| dependencies = [ | ||
@@ -459,0 +459,0 @@ "anyhow", |
+1
-1
| [package] | ||
| name = "practicode" | ||
| version = "0.1.18" | ||
| version = "0.1.19" | ||
| edition = "2024" | ||
@@ -5,0 +5,0 @@ description = "Local-first coding-test practice in a Rust terminal UI with optional AI help." |
+1
-1
| { | ||
| "name": "practicode", | ||
| "version": "0.1.18", | ||
| "version": "0.1.19", | ||
| "description": "Local-first coding-test practice in a Rust terminal UI with optional AI help.", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
+10
-0
@@ -104,2 +104,4 @@ use super::*; | ||
| } | ||
| let mut ids = Vec::new(); | ||
| let mut slugs = Vec::new(); | ||
| for problem in bank { | ||
@@ -117,2 +119,10 @@ if !is_safe_name(&problem.id) { | ||
| } | ||
| if ids.contains(&problem.id.as_str()) { | ||
| bail!("{} has duplicate problem id {}", path.display(), problem.id); | ||
| } | ||
| if slugs.contains(&problem.slug.as_str()) { | ||
| bail!("{} has duplicate slug {}", path.display(), problem.slug); | ||
| } | ||
| ids.push(problem.id.as_str()); | ||
| slugs.push(problem.slug.as_str()); | ||
| if problem.cases.is_empty() { | ||
@@ -119,0 +129,0 @@ bail!( |
@@ -70,3 +70,3 @@ use super::*; | ||
| pub fn normalize_next_source(source: &str) -> String { | ||
| if source == "ai" { | ||
| if source.trim().eq_ignore_ascii_case("ai") { | ||
| "ai".to_string() | ||
@@ -79,3 +79,3 @@ } else { | ||
| pub fn normalize_ai_provider(provider: &str) -> String { | ||
| if provider == "claude" { | ||
| if provider.trim().eq_ignore_ascii_case("claude") { | ||
| "claude".to_string() | ||
@@ -82,0 +82,0 @@ } else { |
+11
-0
@@ -30,2 +30,4 @@ use super::*; | ||
| normalize_settings(&mut state.settings); | ||
| state.suggested_next_difficulty = | ||
| normalize_suggested_difficulty(&state.suggested_next_difficulty); | ||
| state.syntax_progress = normalize_syntax_progress(&state.syntax_progress); | ||
@@ -78,2 +80,3 @@ state.current_syntax_lesson = normalize_current_syntax_lessons(&state.current_syntax_lesson); | ||
| settings.ui_language = normalize_ui_language(&settings.ui_language); | ||
| settings.theme = settings.theme.trim().to_lowercase(); | ||
| if !THEMES.contains(&settings.theme.as_str()) { | ||
@@ -103,1 +106,9 @@ settings.theme = "dark".to_string(); | ||
| } | ||
| fn normalize_suggested_difficulty(difficulty: &str) -> String { | ||
| match normalize_difficulty(difficulty).as_str() { | ||
| "medium" => "medium".to_string(), | ||
| "hard" => "hard".to_string(), | ||
| _ => "easy".to_string(), | ||
| } | ||
| } |
+42
-5
| use anyhow::{Context, Result}; | ||
| use std::{ | ||
| env, | ||
| ffi::OsStr, | ||
| io::{ErrorKind, Write}, | ||
| path::PathBuf, | ||
| path::{Path, PathBuf}, | ||
| process::{Command, Stdio}, | ||
@@ -58,8 +59,29 @@ time::{Duration, SystemTime, UNIX_EPOCH}, | ||
| let paths = env::var_os("PATH")?; | ||
| env::split_paths(&paths).find_map(|dir| { | ||
| let path = dir.join(name); | ||
| if path.is_file() { Some(path) } else { None } | ||
| }) | ||
| let pathext = env::var_os("PATHEXT"); | ||
| env::split_paths(&paths).find_map(|dir| find_in_dir(&dir, name, pathext.as_deref())) | ||
| } | ||
| fn find_in_dir(dir: &Path, name: &str, pathext: Option<&OsStr>) -> Option<PathBuf> { | ||
| let path = dir.join(name); | ||
| if path.is_file() { | ||
| return Some(path); | ||
| } | ||
| if Path::new(name).extension().is_some() { | ||
| return None; | ||
| } | ||
| pathext? | ||
| .to_string_lossy() | ||
| .split(';') | ||
| .filter(|ext| !ext.is_empty()) | ||
| .map(|ext| { | ||
| let ext = if ext.starts_with('.') { | ||
| ext.to_string() | ||
| } else { | ||
| format!(".{ext}") | ||
| }; | ||
| dir.join(format!("{name}{ext}")) | ||
| }) | ||
| .find(|path| path.is_file()) | ||
| } | ||
| pub fn shell_process(command: &str) -> Command { | ||
@@ -106,2 +128,17 @@ if cfg!(windows) { | ||
| } | ||
| #[test] | ||
| fn which_honors_pathext_suffixes() { | ||
| let root = unique_temp_path("practicode-which", "dir"); | ||
| std::fs::create_dir_all(&root).unwrap(); | ||
| let exe = root.join("tool.CMD"); | ||
| std::fs::write(&exe, "").unwrap(); | ||
| assert_eq!( | ||
| find_in_dir(&root, "tool", Some(OsStr::new(".EXE;.CMD"))), | ||
| Some(exe) | ||
| ); | ||
| let _ = std::fs::remove_dir_all(root); | ||
| } | ||
| } |
1266973
0.15%