// The system AnyBot runs on. Most words in the app were written for the Mac ("這台 Mac", // "this Mac"); on Linux (and later Windows) the same sentences say "這台電腦" / "this computer". // `localize` is applied to every translated label (i18n::t) and to what the bot is told (its // instructions and tool descriptions), so the wording follows the system in one place. const PAIRS: &[(&str, &str)] = &[ ("使用者的這台 Mac", "使用者的這台電腦"), ("這台 Mac", "這台電腦"), ("你的 Mac", "你的電腦"), ("使用者的 Mac", "使用者的電腦"), ("我的 Mac", "我的電腦"), ("macOS 鑰匙圈", "系統鑰匙圈"), ("在 Finder 中顯示", "打開所在資料夾"), ("Finder", "檔案管理員"), ("この Mac", "このコンピューター"), ("the person's Mac", "the person's computer"), ("person's Mac", "person's computer"), ("this Mac", "this computer"), ("This Mac", "This computer"), ("your Mac", "your computer"), ("my Mac", "my computer"), ("the Mac", "the computer"), ("macOS Keychain", "system keyring"), ]; /// Mac wording → this system's wording (unchanged on macOS). pub fn localize(s: &str) -> String { // "macOS Keychain" and "macOS 鑰匙圈" contain no "Mac" (lowercase m), so the quick // guard has to test "macOS" as well — those pairs used to never apply. if cfg!(target_os = "macos") || !s.contains("Mac") && !s.contains("macOS") && !s.contains("Finder") { return s.to_string(); } let mut out = s.to_string(); for (a, b) in PAIRS { out = out.replace(a, b); } out } /// Every string inside a JSON value (tool descriptions). #[cfg_attr(target_os = "macos", allow(dead_code))] pub fn localize_value(v: &mut serde_json::Value) { if cfg!(target_os = "macos") { return; } match v { serde_json::Value::String(s) => *s = localize(s), serde_json::Value::Array(a) => a.iter_mut().for_each(localize_value), serde_json::Value::Object(o) => o.values_mut().for_each(localize_value), _ => {} } } #[cfg(test)] mod tests { #[test] fn mac_words_follow_the_system() { let s = super::localize("檔案在這台 Mac;on this Mac"); if cfg!(target_os = "macos") { assert_eq!(s, "檔案在這台 Mac;on this Mac"); } else { assert_eq!(s, "檔案在這台電腦;on this computer"); } } } // ---------- file dialogs on Linux: zenity (GNOME and most desktops) or kdialog (KDE) ---------- fn dialog(zenity: &[&str], kdialog: &[&str]) -> Option { for (cmd, args) in [("zenity", zenity), ("kdialog", kdialog)] { match std::process::Command::new(cmd).args(args).stderr(std::process::Stdio::null()).output() { // Exit code 1 = the person pressed Cancel: don't try the other one. Ok(o) if o.status.success() => return Some(String::from_utf8_lossy(&o.stdout).trim_end_matches('\n').to_string()), Ok(o) if o.status.code() == Some(1) => return None, // Missing binary (Err) or a tool error (zenity 5, kdialog 2): fall through to // the other tool — the old code returned None on any spawn, so a broken zenity // hid the working kdialog. _ => continue, } } None } /// Pick files to open (several at once). pub fn pick_files_linux(title: &str) -> Vec { dialog(&["--file-selection", "--multiple", "--separator=\n", &format!("--title={title}")], &["--getopenfilename", ".", "--multiple", "--separate-output", "--title", title]) .map(|s| s.lines().map(str::trim).filter(|l| !l.is_empty()).map(std::path::PathBuf::from).collect()) .unwrap_or_default() } /// Pick where to save (starting in Downloads with the given name). pub fn choose_save_linux(title: &str, default_name: &str) -> Option { let start = dirs::download_dir().or_else(dirs::home_dir).unwrap_or_default().join(default_name); let start = start.display().to_string(); dialog(&["--file-selection", "--save", "--confirm-overwrite", &format!("--filename={start}"), &format!("--title={title}")], &["--getsavefilename", &start, "--title", title]) .filter(|p| !p.is_empty()) .map(std::path::PathBuf::from) } /// A picture on the clipboard as PNG bytes (wl-paste on Wayland, xclip on X11). pub fn clipboard_png_linux() -> Option> { let wayland = std::env::var("WAYLAND_DISPLAY").map(|v| !v.is_empty()).unwrap_or(false); let (types, get): (Vec<&str>, Vec<&str>) = if wayland { (vec!["wl-paste", "--list-types"], vec!["wl-paste", "--no-newline", "--type", "image/png"]) } else { (vec!["xclip", "-selection", "clipboard", "-t", "TARGETS", "-o"], vec!["xclip", "-selection", "clipboard", "-t", "image/png", "-o"]) }; let list = std::process::Command::new(types[0]).args(&types[1..]).stderr(std::process::Stdio::null()).output().ok()?; if !String::from_utf8_lossy(&list.stdout).lines().any(|l| l.trim() == "image/png") { return None; } let o = std::process::Command::new(get[0]).args(&get[1..]).stderr(std::process::Stdio::null()).output().ok()?; (o.status.success() && o.stdout.starts_with(b"\x89PNG")).then_some(o.stdout) }