From faf1b25afbfe53556d22dbf39c9e1739b396e6af Mon Sep 17 00:00:00 2001 From: esubaalew Date: Mon, 22 Sep 2025 20:41:23 +0300 Subject: [PATCH] feat: robust Wish binary detection with version parsing Improve Wish frontend initialization by adding robust detection of the Wish binary: - Added `wish_parse_version` to extract numeric versions from Wish binary names. - Added `wish_sort_and_dedupe_bins` to sort by version descending and remove duplicate paths. - Added `wish_fallback_candidates` for reliable fallback binaries if PATH scan fails. - Updated `init()` to scan PATH and environment variables (WISH_BIN / AFRISH_WISH_BIN) before falling back. - Added unit tests for parsing, sorting, and fallback functions. This ensures Afrim Wish selects the highest available Wish version, supports custom paths, and gives clear error messages when no valid binary is found. --- Cargo.lock | 10 ++-- src/lib.rs | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 141 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3f3c0b..9d243a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,7 +5,7 @@ version = 3 [[package]] name = "afrim" version = "0.6.0" -source = "git+https://github.com/pythonbrad/afrim?rev=5f40469#5f40469dd3970c68ede03f7d0041fc4644f2e59a" +source = "git+https://github.com/fodydev/afrim?rev=5f40469#5f40469dd3970c68ede03f7d0041fc4644f2e59a" dependencies = [ "afrim-config", "afrim-preprocessor", @@ -19,7 +19,7 @@ dependencies = [ [[package]] name = "afrim-config" version = "0.4.5" -source = "git+https://github.com/pythonbrad/afrim?rev=5f40469#5f40469dd3970c68ede03f7d0041fc4644f2e59a" +source = "git+https://github.com/fodydev/afrim?rev=5f40469#5f40469dd3970c68ede03f7d0041fc4644f2e59a" dependencies = [ "anyhow", "indexmap", @@ -31,12 +31,12 @@ dependencies = [ [[package]] name = "afrim-memory" version = "0.4.2" -source = "git+https://github.com/pythonbrad/afrim?rev=5f40469#5f40469dd3970c68ede03f7d0041fc4644f2e59a" +source = "git+https://github.com/fodydev/afrim?rev=5f40469#5f40469dd3970c68ede03f7d0041fc4644f2e59a" [[package]] name = "afrim-preprocessor" version = "0.6.1" -source = "git+https://github.com/pythonbrad/afrim?rev=5f40469#5f40469dd3970c68ede03f7d0041fc4644f2e59a" +source = "git+https://github.com/fodydev/afrim?rev=5f40469#5f40469dd3970c68ede03f7d0041fc4644f2e59a" dependencies = [ "afrim-memory", "keyboard-types", @@ -45,7 +45,7 @@ dependencies = [ [[package]] name = "afrim-translator" version = "0.2.1" -source = "git+https://github.com/pythonbrad/afrim?rev=5f40469#5f40469dd3970c68ede03f7d0041fc4644f2e59a" +source = "git+https://github.com/fodydev/afrim?rev=5f40469#5f40469dd3970c68ede03f7d0041fc4644f2e59a" dependencies = [ "indexmap", "rhai", diff --git a/src/lib.rs b/src/lib.rs index e8b25e7..9bb2a96 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,11 +22,108 @@ pub struct Wish { } impl Wish { + // Extract a version from a wish binary name. + // Examples: + // - "wish" => Some(0.0) + // - "wish9.0" => Some(9.0) + // - "wish8.6" => Some(8.6) + // - "wishx" => None (ignored) + pub(crate) fn wish_parse_version(name: &str) -> Option { + if !name.starts_with("wish") { + return None; + } + + let tail = &name[4..]; + let ver_str: String = tail + .chars() + .take_while(|c| c.is_ascii_digit() || *c == '.') + .collect(); + + if tail.is_empty() { + return Some(0.0); + } + + if ver_str.is_empty() { + return None; + } + + Some(ver_str.parse::().unwrap_or(0.0)) + } + + pub(crate) fn wish_sort_and_dedupe_bins(mut bins: Vec<(f32, String)>) -> Vec { + bins.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal)); + let mut seen = std::collections::HashSet::new(); + let mut candidates: Vec = Vec::new(); + for (_, p) in bins { + if seen.insert(p.clone()) { + candidates.push(p); + } + } + candidates + } + + pub(crate) fn wish_fallback_candidates() -> Vec { + vec!["wish9.0".into(), "wish8.7".into(), "wish8.6".into(), "wish".into()] + } + fn init() -> &'static afrish::TkTopLevel { static WISH: OnceLock = OnceLock::new(); WISH.get_or_init(|| { let wish = if cfg!(debug_assertions) { - afrish::trace_with("wish").unwrap() + // Prefer a user-provided Wish binary on macOS where /usr/bin/wish is often Tk 8.5. + let env_bin = std::env::var("WISH_BIN") + .or_else(|_| std::env::var("AFRISH_WISH_BIN")) + .ok(); + + if let Some(bin) = env_bin { + afrish::trace_with(&bin).unwrap_or_else(|e| { + panic!( + "Failed to start Wish at '{}'. Error: {:?}", + bin, e + ) + }) + } else { + let mut bins: Vec<(f32, String)> = Vec::new(); + if let Ok(path_var) = std::env::var("PATH") { + for dir in std::env::split_paths(&path_var) { + if let Ok(read_dir) = std::fs::read_dir(&dir) { + for entry in read_dir.flatten() { + let name_os = entry.file_name(); + if let Some(name) = name_os.to_str() { + if let Some(ver) = Self::wish_parse_version(name) { + let full = entry.path().to_string_lossy().to_string(); + bins.push((ver, full)); + } + } + } + } + } + } + + let mut candidates = Self::wish_sort_and_dedupe_bins(bins); + if candidates.is_empty() { + // Fallback to common names if PATH scan found nothing. + candidates = Self::wish_fallback_candidates(); + } + + let mut last_err: Option<(String, afrish::TkError)> = None; + for c in candidates { + match afrish::trace_with(&c) { + Ok(w) => return w, + Err(e) => last_err = Some((c, e)), + } + } + + match last_err { + Some((cmd, err)) => panic!( + "Failed to launch Wish (last tried '{}'). Set WISH_BIN to your Tk wish (e.g., /opt/homebrew/opt/tcl-tk/bin/wish or wish9.0). Error: {:?}", + cmd, err + ), + None => panic!( + "Failed to launch Wish. Set WISH_BIN to your Tk wish (e.g., /opt/homebrew/opt/tcl-tk/bin/wish or wish9.0)." + ), + } + } } else { afrish::start_wish().unwrap() }; @@ -147,6 +244,43 @@ mod tests { use std::thread; use std::time::Duration; + #[test] + fn test_wish_parse_version() { + assert_eq!(Wish::wish_parse_version("wish"), Some(0.0)); + assert_eq!(Wish::wish_parse_version("wish9.0"), Some(9.0)); + assert_eq!(Wish::wish_parse_version("wish8.6"), Some(8.6)); + assert_eq!(Wish::wish_parse_version("wish10"), Some(10.0)); + assert_eq!(Wish::wish_parse_version("wishx"), None); + assert_eq!(Wish::wish_parse_version("bash"), None); + } + + #[test] + fn test_wish_sort_and_dedupe_bins() { + let bins = vec![ + (8.6, "/bin/wish8.6".to_string()), + (9.0, "/opt/wish9.0".to_string()), + (0.0, "/usr/bin/wish".to_string()), + (9.0, "/opt/wish9.0".to_string()), // duplicate path + ]; + let out = Wish::wish_sort_and_dedupe_bins(bins); + assert_eq!(out, vec![ + "/opt/wish9.0".to_string(), + "/bin/wish8.6".to_string(), + "/usr/bin/wish".to_string(), + ]); + } + + #[test] + fn test_wish_fallback_candidates() { + let fall = Wish::wish_fallback_candidates(); + assert_eq!(fall, vec![ + "wish9.0".to_string(), + "wish8.7".to_string(), + "wish8.6".to_string(), + "wish".to_string(), + ]); + } + #[test] fn test_api() { let config = Config::from_file(Path::new("data/full_sample.toml")).unwrap(); @@ -260,4 +394,4 @@ mod tests { // We wait the afrim to end properly. afrim_wish_thread.join().unwrap(); } -} +} \ No newline at end of file