// 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. /// A screen the person sits at (not a server with no display). pub fn has_desktop() -> bool { if cfg!(target_os = "linux") { std::env::var("DISPLAY").map(|v| !v.is_empty()).unwrap_or(false) || std::env::var("WAYLAND_DISPLAY").map(|v| !v.is_empty()).unwrap_or(false) } else { true } } 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 { if cfg!(target_os = "macos") || !s.contains("Mac") && !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)] { if let Ok(o) = 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. return o.status.success().then(|| String::from_utf8_lossy(&o.stdout).trim_end_matches('\n').to_string()); } } 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) }