117 lines
5.0 KiB
Rust
117 lines
5.0 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.
|
|||
|
|
|
|||
|
|
/// 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<String> {
|
|||
|
|
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<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)
|
|||
|
|
}
|