115 lines
5.2 KiB
Rust
115 lines
5.2 KiB
Rust
// 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<String> {
|
||
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<std::path::PathBuf> {
|
||
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<std::path::PathBuf> {
|
||
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<Vec<u8>> {
|
||
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)
|
||
}
|