From 4506cc149b223063aed21505d22837d3db3d1db7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E6=80=A7=E9=A9=8A?= Date: Sat, 7 Nov 2026 10:10:28 +0800 Subject: [PATCH] Clippy is clean on macOS and Linux, for the app and the relay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every warning `cargo clippy --all-targets` gave (50 on macOS, 8 more only on Linux, 1 in the relay), fixed without changing what anything does: - long types get a name (Peer, Members, Imported, Joining, Mention, Stream…); - the two big enum variants are boxed (the vault's login plan, joining an account); - a test now waits for the copy it relaunched (it left a zombie behind); - a hint's format had a dead `if` whose two branches were both ""; - the rest is what clippy suggests: sort_by_key, as_chunks, is_none_or, is_multiple_of, while let, from_ref, next_back, struct init, no casts to the same type. Checked: cargo test (177 macOS, 181 Linux), relay tests (11), remote_e2e 28/28, and the live Linux test (X11; Wayland clicks, keys, Chinese typing, scroll and screenshots). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uirha2RVfSYmrhqNnqx9Z4 --- relay/src/main.rs | 5 ++++- src/agent.rs | 8 ++++---- src/app.rs | 14 ++++++-------- src/app/channels_ui.rs | 2 +- src/app/computers_ui.rs | 9 ++++++--- src/app/markdown_ui.rs | 4 +++- src/app/mirror_ui.rs | 5 ++++- src/app/panel_ui.rs | 2 +- src/app/remote_ui.rs | 5 ++++- src/app/sidebar_ui.rs | 2 +- src/app/vault_ui.rs | 5 ++++- src/cloudbox.rs | 4 +++- src/computer.rs | 4 ++-- src/diagram.rs | 6 ++---- src/macapps.rs | 2 +- src/main.rs | 2 +- src/notify.rs | 4 ++-- src/pages.rs | 5 ++--- src/phone.rs | 12 +++++++++--- src/platform.rs | 26 +++++++++++++------------- src/preview.rs | 7 +++---- src/remote.rs | 8 +++++--- src/routines.rs | 7 +++---- src/setup.rs | 11 +++++++---- src/teach.rs | 4 ++-- src/tools.rs | 4 ++-- src/vault.rs | 2 +- src/vault_store.rs | 8 ++++---- src/voice.rs | 2 +- src/web.rs | 2 +- src/wl.rs | 4 ++-- src/x11.rs | 7 +++---- 32 files changed, 107 insertions(+), 85 deletions(-) diff --git a/relay/src/main.rs b/relay/src/main.rs index c4fb037..d838799 100644 --- a/relay/src/main.rs +++ b/relay/src/main.rs @@ -187,6 +187,9 @@ struct Sock { static SERIAL: AtomicU64 = AtomicU64::new(1); +/// A screen being watched: where the host's side goes, and that host. +type Stream = (Sender>, String); + struct Relay { dir: PathBuf, data: Mutex, @@ -204,7 +207,7 @@ struct Relay { uses: Mutex>, hands: Mutex>, /// People watching a bots' computer: stream id → (where the host's side goes, that host). - streams: Mutex>, String)>>, + streams: Mutex>, /// One-time links for a browser to watch a host's computer: ticket → (account, host, made). tickets: Mutex>, /// 配對碼: code → (account, the AnyBot it is for, when made). Ten minutes, once. diff --git a/src/agent.rs b/src/agent.rs index 0ccaeb3..63935aa 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -1037,7 +1037,7 @@ pub fn spawn(job: Job, bot_id: u64, tx: Sender<(u64, StreamEvent)>, cancel: Arc< } if call.name == crate::vault::TOOL || call.name == crate::vault::CARD_TOOL || call.name == crate::vault::CONTACT_TOOL { enum Plan { - Login(crate::vault::LoginPlan), + Login(Box), Card, Contact, } @@ -1045,7 +1045,7 @@ pub fn spawn(job: Job, bot_id: u64, tx: Sender<(u64, StreamEvent)>, cancel: Arc< crate::vault::plan_login(&call.args).map(|pl| { let q = pl.question(); let grant = if pl.matched { format!("login:{}", pl.host) } else { String::new() }; - (Plan::Login(pl), q, grant) + (Plan::Login(Box::new(pl)), q, grant) }) } else if call.name == crate::vault::CARD_TOOL { crate::vault::plan_card(&call.args).map(|(host, c)| (Plan::Card, format!("用保險庫的卡片({} •••• {})在 {host} 填入付款資料(Bot 看不到卡號和安全碼)", c.display_title(), c.last4), String::new())) @@ -1855,8 +1855,8 @@ echo 'done送出了' > done.html; (setsid pyt #[test] fn a_click_by_number_is_named_in_the_question() { let list = Message::tool_result("c1", "computer_use_screenshot", "[王小明的Mac mini] 螢幕截圖(1024×576)。\n「計算機」上可以操作的東西(圖上同樣的編號,用 ref 點):\n 文字「0」\n[4] 按鈕「全部清除」\n[7] 按鈕「7」"); - assert_eq!(element_named(&[list.clone()], "7").as_deref(), Some("按鈕「7」")); - assert_eq!(element_named(&[list.clone()], "e7").as_deref(), Some("按鈕「7」")); + assert_eq!(element_named(std::slice::from_ref(&list), "7").as_deref(), Some("按鈕「7」")); + assert_eq!(element_named(std::slice::from_ref(&list), "e7").as_deref(), Some("按鈕「7」")); assert_eq!(element_named(&[list], "9"), None); assert_eq!(computer::describe("computer_use_click", r#"{"ref":"7"}"#), "點 #7"); assert_eq!(computer::describe("computer_use_click", r#"{"x":10,"y":20}"#), "點 (10, 20)"); diff --git a/src/app.rs b/src/app.rs index 96830c6..d460768 100644 --- a/src/app.rs +++ b/src/app.rs @@ -571,9 +571,8 @@ impl App { } // …or the side panel, or the computer viewer, as they look with that data. if app.screenshot.is_some() && !app.demo { - match std::env::var("ANYBOT_SHOT_VIEW").as_deref() { - Ok("panel") => app.panel = Some(panel_ui::PanelTab::Activity), - _ => {} + if let Ok("panel") = std::env::var("ANYBOT_SHOT_VIEW").as_deref() { + app.panel = Some(panel_ui::PanelTab::Activity); } } app @@ -2597,10 +2596,8 @@ impl App { ui.label(RichText::new(t("你平常的 Chrome,切到哪個設定檔就讀哪個")).size(11.5).color(p.text2)); if crate::axread::trusted(false) { crate::icons::label(ui, crate::icons::Icon::Check, &t("輔助使用已打開"), 11.5, p.text3); - } else if ui.small_button(t("打開輔助使用")).on_hover_text(t("macOS 要你允許一次,AnyBot 才看得到你的瀏覽器視窗")).clicked() { - if !crate::axread::trusted(true) { - let _ = crate::computer::spawn_reaped(&mut std::process::Command::new("open").arg("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")); - } + } else if ui.small_button(t("打開輔助使用")).on_hover_text(t("macOS 要你允許一次,AnyBot 才看得到你的瀏覽器視窗")).clicked() && !crate::axread::trusted(true) { + let _ = crate::computer::spawn_reaped(std::process::Command::new("open").arg("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")); } return; } @@ -4451,12 +4448,13 @@ mod tests { let mut old = std::process::Command::new("sleep").arg("1").spawn().unwrap(); // A leftover start-up switch in this copy's environment must not reach the new copy. std::env::set_var("ANYBOT_SHOT_VIEW", "leftover"); - relaunch_command(old.id(), &fake_app).spawn().unwrap(); + let mut relaunched = relaunch_command(old.id(), &fake_app).spawn().unwrap(); std::env::remove_var("ANYBOT_SHOT_VIEW"); std::thread::sleep(std::time::Duration::from_millis(500)); assert!(!marker.exists(), "started before the old copy exited"); old.wait().unwrap(); std::thread::sleep(std::time::Duration::from_millis(700)); assert_eq!(std::fs::read_to_string(&marker).unwrap().trim(), "clean"); + relaunched.wait().unwrap(); } } diff --git a/src/app/channels_ui.rs b/src/app/channels_ui.rs index d1687af..b5bf072 100644 --- a/src/app/channels_ui.rs +++ b/src/app/channels_ui.rs @@ -158,7 +158,7 @@ impl App { }); }); let bot = bots.iter().find(|b| b.0 == c.bot_id).map(|b| b.1.clone()).unwrap_or_else(|| "(Bot 已刪除)".into()); - hint(ui, format!("由「{bot}」回覆 · {}{}", if c.target.is_empty() { "任何聊天".to_string() } else { c.target.clone() }, if c.kind == Kind::Telegram { "" } else { "" }), p); + hint(ui, format!("由「{bot}」回覆 · {}", if c.target.is_empty() { "任何聊天".to_string() } else { c.target.clone() }), p); }); } if let Some(i) = remove { diff --git a/src/app/computers_ui.rs b/src/app/computers_ui.rs index 9602523..82e25d2 100644 --- a/src/app/computers_ui.rs +++ b/src/app/computers_ui.rs @@ -10,12 +10,15 @@ use crate::cloudbox::{self, Rebuild, Run}; /// The floating bar stays this long after the bot's last mouse/keyboard action on this Mac. const LOCAL_BAR_SECS: u64 = 8; +/// Another AnyBot of the account: (id, name, online). +pub type Peer = (String, String, bool); + #[derive(Default)] pub struct ComputerUi { /// 其他電腦 is expanded. pub others_open: bool, /// The account's other AnyBots (id, name, online), fetched from the relay now and then. - pub peers: Arc>>>, + pub peers: Arc>>>, pub peers_at: Option, /// 你的電腦's name being edited. pub renaming_machine: Option, @@ -101,7 +104,7 @@ impl App { let p = self.pal; let run = cloudbox::run_state(); // Check now and then whether this AnyBot carries a newer computer. - if !self.demo && self.cu.update_checked.map_or(true, |t| t.elapsed().as_secs() > 30) { + if !self.demo && self.cu.update_checked.is_none_or(|t| t.elapsed().as_secs() > 30) { self.cu.update_checked = Some(Instant::now()); let flag = self.cu.update_flag.clone(); let facts = self.cu.runtime_facts.clone(); @@ -216,7 +219,7 @@ impl App { } // 其他電腦: every AnyBot signed in to this account (from the relay), then SSH computers. let signed_in = !self.state.remote.relay_url.is_empty() && !self.state.remote.host_token.is_empty(); - if signed_in && !self.demo && self.cu.peers_at.map_or(true, |t| t.elapsed().as_secs() > 20) { + if signed_in && !self.demo && self.cu.peers_at.is_none_or(|t| t.elapsed().as_secs() > 20) { self.cu.peers_at = Some(Instant::now()); let (slot, settings) = (self.cu.peers.clone(), self.state.remote.clone()); let ctx = ui.ctx().clone(); diff --git a/src/app/markdown_ui.rs b/src/app/markdown_ui.rs index 8422933..fd4e208 100644 --- a/src/app/markdown_ui.rs +++ b/src/app/markdown_ui.rs @@ -72,7 +72,9 @@ pub fn parse(text: &str) -> Vec { let mut in_inline: Option> = None; // Some(None)=paragraph, Some(Some(n))=heading let mut code: Option<(String, String)> = None; // Tables. - let mut table: Option<(Vec, Vec>, Vec>>, bool)> = None; + // (column alignments, header cells, body rows, inside the header) + type Table = (Vec, Vec>, Vec>>, bool); + let mut table: Option = None; let mut row: Vec> = vec![]; fn push_block(stack: &mut [Frame], b: Block) { diff --git a/src/app/mirror_ui.rs b/src/app/mirror_ui.rs index a6c95c0..0461eaf 100644 --- a/src/app/mirror_ui.rs +++ b/src/app/mirror_ui.rs @@ -353,6 +353,9 @@ enum Out { Move { ops: Vec, n: usize }, } +/// An edit sent to the brain and not yet confirmed: (its number, the operations). +type Pending = (u64, Vec); + #[derive(Default)] pub struct Mirror { /// This window shows a brain (instead of its own bots). @@ -382,7 +385,7 @@ pub struct Mirror { /// Requests to the brain, sent in order by one worker. outbox: Option>, /// Edits sent but not yet confirmed (by number). - pending: Arc)>>>, + pending: Arc>>, seq: u64, /// Something didn't reach the brain: ask for everything again. resync: Arc, diff --git a/src/app/panel_ui.rs b/src/app/panel_ui.rs index df01a27..277ed5b 100644 --- a/src/app/panel_ui.rs +++ b/src/app/panel_ui.rs @@ -356,7 +356,7 @@ impl App { } } } - files.sort_by(|a, b| b.2.cmp(&a.2)); + files.sort_by_key(|f| std::cmp::Reverse(f.2)); // 頁面 the bot made come first. let has_pages = self.pages_library(ui, bot); if files.is_empty() && has_pages { diff --git a/src/app/remote_ui.rs b/src/app/remote_ui.rs index 071a000..bb25ca7 100644 --- a/src/app/remote_ui.rs +++ b/src/app/remote_ui.rs @@ -7,6 +7,9 @@ use crate::remote::{self, Cmd, Settings}; use serde_json::json; use std::hash::{Hash, Hasher}; +/// 團隊 as the relay answered: each (user, role), or why it could not be loaded. +type Members = Result, String>; + #[derive(Default)] pub struct RemoteUi { sig: u64, @@ -28,7 +31,7 @@ pub struct RemoteUi { last_seen_mark: Option, peers_at: Option, /// 團隊: (user, role) shared with, loaded from the relay; the form to add one. - members: Arc, String>>>>, + members: Arc>>, new_member: String, new_admin: bool, started: bool, diff --git a/src/app/sidebar_ui.rs b/src/app/sidebar_ui.rs index 21905b6..346b3ab 100644 --- a/src/app/sidebar_ui.rs +++ b/src/app/sidebar_ui.rs @@ -255,7 +255,7 @@ impl App { let q = self.search.trim().to_lowercase(); let matches = |b: &Bot| q.is_empty() || b.name.to_lowercase().contains(&q) || b.description.to_lowercase().contains(&q); let primary = self.state.bot(self.state.primary_bot).filter(|b| matches(b)).map(|b| b.id); - let pins: Vec = self.pinned_ids().into_iter().filter(|id| self.state.bot(*id).map(|b| matches(b)).unwrap_or(false)).collect(); + let pins: Vec = self.pinned_ids().into_iter().filter(|id| self.state.bot(*id).is_some_and(matches)).collect(); let folders = self.all_folders(); let has_sections = !folders.is_empty(); // 分區 in order (None = 未分區); pinned bots show only in the grid. diff --git a/src/app/vault_ui.rs b/src/app/vault_ui.rs index ed13360..1c3f493 100644 --- a/src/app/vault_ui.rs +++ b/src/app/vault_ui.rs @@ -56,6 +56,9 @@ pub struct ContactForm { pub err: String, } +/// An import's result: (logins, cards, the ones that failed with why). +pub type Imported = (usize, usize, Vec<(String, String)>); + #[derive(Default)] pub struct ImportState { pub picking: Option>>>>, @@ -66,7 +69,7 @@ pub struct ImportState { pub queue: Vec, pub total: usize, pub stop: bool, - pub done: Option<(usize, usize, Vec<(String, String)>)>, + pub done: Option, } pub enum VaultDialog { diff --git a/src/cloudbox.rs b/src/cloudbox.rs index 4ed883e..54be4e5 100644 --- a/src/cloudbox.rs +++ b/src/cloudbox.rs @@ -1594,7 +1594,9 @@ fn box_scale() -> Result<(f64, f64), String> { /// steps in one exec with one stdin, so steps can't ride on it). pub fn box_script(name: &str, v: &Value, scale: (f64, f64)) -> Result<(String, String), String> { // A control's number from the latest picture: its centre on the desktop. - let by_number = |p: &Value| -> Option> { + // (the point to click, what it is) or why there is none. + type Found = Result<((i64, i64), String), String>; + let by_number = |p: &Value| -> Option { let r = match &p["ref"] { Value::String(r) if !r.trim().is_empty() => r.trim().to_string(), Value::Number(n) => n.to_string(), diff --git a/src/computer.rs b/src/computer.rs index a9b1297..cd7f69b 100644 --- a/src/computer.rs +++ b/src/computer.rs @@ -357,7 +357,7 @@ pub fn encode_jpeg(img: &image::RgbImage) -> Option> { if out.len() <= MAX_IMAGE_BYTES { return Some(out); } - if best.as_ref().map_or(true, |b| out.len() < b.len()) { + if best.as_ref().is_none_or(|b| out.len() < b.len()) { best = Some(out); } } @@ -517,7 +517,7 @@ pub fn ask_permission(pane: &str) { } _ => {} } - let _ = spawn_reaped(&mut std::process::Command::new("open").arg(format!("x-apple.systempreferences:com.apple.preference.security?{pane}"))); + let _ = spawn_reaped(std::process::Command::new("open").arg(format!("x-apple.systempreferences:com.apple.preference.security?{pane}"))); } const NO_ACCESSIBILITY: &str = "macOS 還沒讓 AnyBot 使用「輔助使用」,所以不能讀 App,也不能動滑鼠和鍵盤。這台電腦上已經跳出詢問並打開「系統設定 → 隱私權與安全性 → 輔助使用」:請使用者把 AnyBot 打開(只要一次),再叫我試一次。"; diff --git a/src/diagram.rs b/src/diagram.rs index 6304db1..2c7b72a 100644 --- a/src/diagram.rs +++ b/src/diagram.rs @@ -43,9 +43,7 @@ pub fn svg(source: &str, dark: bool) -> Result { /// Mermaid source → RGBA pixels at `scale` (2.0 for sharp text on Retina). pub fn render(source: &str, dark: bool, scale: f32) -> Result { let svg = svg(source, dark)?; - let mut opt = resvg::usvg::Options::default(); - opt.fontdb = fontdb(); - opt.font_family = "sans-serif".into(); + let opt = resvg::usvg::Options { fontdb: fontdb(), font_family: "sans-serif".into(), ..Default::default() }; let tree = resvg::usvg::Tree::from_str(&svg, &opt).map_err(|e| e.to_string())?; let size = tree.size(); let (w, h) = ((size.width() * scale).ceil().max(1.0) as u32, (size.height() * scale).ceil().max(1.0) as u32); @@ -56,7 +54,7 @@ pub fn render(source: &str, dark: bool, scale: f32) -> Result().0 { let a = c[3] as u32; if a > 0 && a < 255 { for v in &mut c[..3] { diff --git a/src/macapps.rs b/src/macapps.rs index 1c4f676..4bce9d3 100644 --- a/src/macapps.rs +++ b/src/macapps.rs @@ -185,7 +185,7 @@ fn jxa(script: &str, args: &Value) -> Result { fn permission_hint(app: &str, err: &str) -> String { let e = err.to_lowercase(); if e.contains("-1743") || e.contains("not allowed") || e.contains("不允許") || e.contains("not authorized") { - let _ = crate::computer::spawn_reaped(&mut std::process::Command::new("open").arg("x-apple.systempreferences:com.apple.preference.security?Privacy_Automation")); + let _ = crate::computer::spawn_reaped(std::process::Command::new("open").arg("x-apple.systempreferences:com.apple.preference.security?Privacy_Automation")); return format!("AnyBot 還不能控制這台 Mac 的「{app}」。我已經打開「系統設定 → 隱私權與安全性 → 自動化」:在 AnyBot(或「終端機」)底下打開「{app}」,再叫我試一次。"); } format!("「{app}」回報錯誤:{}", err.lines().last().unwrap_or(err)) diff --git a/src/main.rs b/src/main.rs index c843bf3..fbf75b3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -160,7 +160,7 @@ fn svg_icon(svg: &str, px: u32) -> egui::IconData { resvg::render(&tree, resvg::tiny_skia::Transform::from_scale(s, s), &mut pm.as_mut()); // tiny-skia is premultiplied; IconData wants straight alpha. let mut rgba = pm.take(); - for c in rgba.chunks_exact_mut(4) { + for c in rgba.as_chunks_mut::<4>().0 { let a = c[3] as u32; if a > 0 && a < 255 { for v in &mut c[..3] { diff --git a/src/notify.rs b/src/notify.rs index d3ba116..ae92a1c 100644 --- a/src/notify.rs +++ b/src/notify.rs @@ -117,10 +117,10 @@ pub fn banner(title: &str, body: &str) { if !cfg!(target_os = "macos") { // The desktop's notification service (GNOME, KDE, XFCE…) through notify-send. let body: String = body.chars().take(180).collect(); - let _ = crate::computer::spawn_reaped(&mut Command::new("notify-send").args(["-a", "AnyBot", title, &body]).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null())); + let _ = crate::computer::spawn_reaped(Command::new("notify-send").args(["-a", "AnyBot", title, &body]).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null())); return; } - let _ = crate::computer::spawn_reaped(&mut Command::new("osascript").arg("-e").arg(banner_script(title, body)).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null())); + let _ = crate::computer::spawn_reaped(Command::new("osascript").arg("-e").arg(banner_script(title, body)).stdin(Stdio::null()).stdout(Stdio::null()).stderr(Stdio::null())); } #[cfg(test)] diff --git a/src/pages.rs b/src/pages.rs index 26cf739..70975d6 100644 --- a/src/pages.rs +++ b/src/pages.rs @@ -335,10 +335,9 @@ pub fn steps(src: &str) -> Vec<(Step, String, String)> { (Step::Done, r) } else if let Some(r) = t.strip_prefix("[>]").or_else(|| t.strip_prefix("[~]")).or_else(|| t.strip_prefix("[-]")) { (Step::Now, r) - } else if let Some(r) = t.strip_prefix("[ ]") { - (Step::Todo, r) } else { - return None; + let r = t.strip_prefix("[ ]")?; + (Step::Todo, r) }; let mut c = rest.splitn(2, '|'); let name = plain(c.next().unwrap_or("").trim()); diff --git a/src/phone.rs b/src/phone.rs index 73f2524..7f4f7e3 100644 --- a/src/phone.rs +++ b/src/phone.rs @@ -133,8 +133,11 @@ pub fn x_post(token: &str, text: &str, reply_to: &str) -> Result Ok(format!("https://x.com/i/status/{}", v["data"]["id"].as_str().unwrap_or(""))) } -/// Mentions newer than `since_id`, oldest first: (id, author username, text). Returns the newest id. -pub fn x_mentions(token: &str, user_id: &str, since_id: &str) -> Result<(String, Vec<(String, String, String)>), String> { +/// A mention on X: (id, author username, text). +pub type Mention = (String, String, String); + +/// Mentions newer than `since_id`, oldest first. Returns the newest id. +pub fn x_mentions(token: &str, user_id: &str, since_id: &str) -> Result<(String, Vec), String> { let q = if since_id.is_empty() { String::new() } else { format!("&since_id={since_id}") }; let v = x_get(token, &format!("/2/users/{user_id}/mentions?max_results=20&expansions=author_id&user.fields=username{q}"))?; Ok(mentions_new(&v, since_id)) @@ -220,8 +223,11 @@ mod tests { use super::*; use std::io::{Read, Write}; + /// Requests the stand-in got: (request line, Authorization header, body). + type Requests = std::sync::Arc>>; + /// A stand-in for Twilio and X: records requests, answers like they do. - fn fake() -> (u16, std::sync::Arc>>) { + fn fake() -> (u16, Requests) { let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); let port = l.local_addr().unwrap().port(); let log = std::sync::Arc::new(Mutex::new(vec![])); diff --git a/src/platform.rs b/src/platform.rs index 2a5c862..529437b 100644 --- a/src/platform.rs +++ b/src/platform.rs @@ -51,19 +51,6 @@ pub fn localize_value(v: &mut serde_json::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 { @@ -112,3 +99,16 @@ pub fn clipboard_png_linux() -> Option> { 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) } + +#[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"); + } + } +} diff --git a/src/preview.rs b/src/preview.rs index 632cf3e..62e2ceb 100644 --- a/src/preview.rs +++ b/src/preview.rs @@ -168,7 +168,7 @@ pub fn decode_image(path: &Path) -> Option { resvg::render(&tree, resvg::tiny_skia::Transform::from_scale(scale, scale), &mut pm.as_mut()); // tiny-skia keeps premultiplied pixels. let mut px = pm.take(); - for c in px.chunks_exact_mut(4) { + for c in px.as_chunks_mut::<4>().0 { let a = c[3] as u32; if a > 0 && a < 255 { for v in &mut c[..3] { @@ -233,7 +233,7 @@ pub fn pdf_pages(path: &Path, width: f32, max_pages: usize, mut each: impl FnMut let (w, h) = (pm.width() as u32, pm.height() as u32); let mut px = pm.take_rgba8(hayro::vello_cpu::peniko::ImageAlphaType::AlphaPremultiplied); // Paper is opaque: composite onto white (premultiplied: add what alpha left uncovered). - for c in px.chunks_exact_mut(4) { + for c in px.as_chunks_mut::<4>().0 { let rest = 255 - c[3]; c[0] = c[0].saturating_add(rest); c[1] = c[1].saturating_add(rest); @@ -490,8 +490,7 @@ fn find_ci(hay: &str, needle: &str) -> Option { pub fn html_markdown(html: &str) -> String { let mut s = html.to_string(); for tag in ["head", "script", "style", "noscript", "svg", "template"] { - loop { - let Some(a) = find_open_tag(&s, tag) else { break }; + while let Some(a) = find_open_tag(&s, tag) { let end = find_ci(&s[a..], &format!("")).map(|e| a + e + tag.len() + 3).unwrap_or(s.len()); s.replace_range(a..end, ""); } diff --git a/src/remote.rs b/src/remote.rs index 43eb4f9..93b6712 100644 --- a/src/remote.rs +++ b/src/remote.rs @@ -331,7 +331,7 @@ fn devices_ok(token: &str) -> Option { /// `via_relay`: the relay already checked the account, so no phone password is needed. pub fn handle(req: Req, via_relay: bool) -> Resp { let parts = req.parts(); - let path: Vec<&str> = parts.iter().map(|s| *s).collect(); + let path: Vec<&str> = parts.to_vec(); match (req.method.as_str(), path.as_slice()) { ("GET", []) | ("GET", ["index.html"]) => return page(), ("GET", ["screen"]) => return Resp::bytes("text/html; charset=utf-8", SCREEN_PAGE.as_bytes().to_vec()).with_header("Content-Security-Policy", "default-src 'self'; img-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:"), @@ -1558,8 +1558,10 @@ fn hands_ids() -> &'static Mutex> { } /// The account's computers as last seen: id → (name, the brain it uses, lends its hands). -fn peer_rows() -> &'static Mutex> { - static P: OnceLock>> = OnceLock::new(); +type PeerRows = Mutex>; + +fn peer_rows() -> &'static PeerRows { + static P: OnceLock = OnceLock::new(); P.get_or_init(Mutex::default) } diff --git a/src/routines.rs b/src/routines.rs index 209381e..593ec02 100644 --- a/src/routines.rs +++ b/src/routines.rs @@ -323,15 +323,14 @@ fn exact_next(s: &Schedule, created: i64, after: i64) -> Option { let tz = tz_of(&s.timezone); let at = Utc.timestamp_opt(created, 0).single()?.with_timezone(&tz); // Offset seconds of that zone at `created` (local wall clock minus UTC). - let off = (at.naive_local() - at.naive_utc()).num_seconds() as i64; + let off = (at.naive_local() - at.naive_utc()).num_seconds(); let hour0 = (created + off).div_euclid(3600); - let mut h = (after + off).div_euclid(3600); - for _ in 0..(n * 2 + 2) { + let first = (after + off).div_euclid(3600); + for h in first..first + n * 2 + 2 { let ts = h * 3600 + minute * 60 - off; if ts > after && (h - hour0).rem_euclid(n) == 0 { return Some(ts); } - h += 1; } return None; } diff --git a/src/setup.rs b/src/setup.rs index 9a381ed..53673a5 100644 --- a/src/setup.rs +++ b/src/setup.rs @@ -88,9 +88,12 @@ enum Mark { Fail, } +/// Joining an account: its settings, the brain a code was made for, the brains to choose from. +type Joining = Result<(Settings, Option, Vec), String>; + enum Msg { /// Joined the account; with a code, the brain it was made for. - Joined(Result<(Settings, Option, Vec), String>), + Joined(Box), Progress(usize, Mark, String), Finished(Vec), } @@ -397,7 +400,7 @@ impl Wiz { self.error = "請輸入帳號和密碼".into(); return; } - self.start("正在登入…", move || Msg::Joined(join_with_password(&base, &url, &user, &pass))); + self.start("正在登入…", move || Msg::Joined(Box::new(join_with_password(&base, &url, &user, &pass)))); } else { let code: String = self.code.chars().filter(|c| c.is_ascii_digit()).collect(); if code.len() != 6 { @@ -405,7 +408,7 @@ impl Wiz { self.focus = 1; return; } - self.start("正在連線…", move || Msg::Joined(remote::pair_claim(&base, &url, &code).map(|s| (s.clone(), brain_of(&s), vec![])))); + self.start("正在連線…", move || Msg::Joined(Box::new(remote::pair_claim(&base, &url, &code).map(|s| (s.clone(), brain_of(&s), vec![]))))); } } @@ -421,7 +424,7 @@ impl Wiz { match m { Msg::Joined(r) => { self.busy = None; - match r { + match *r { Ok((s, brain, choices)) => { self.joined = Some(s); self.pass.clear(); diff --git a/src/teach.rs b/src/teach.rs index 588982f..1c89bcc 100644 --- a/src/teach.rs +++ b/src/teach.rs @@ -481,7 +481,7 @@ mod mac { match rx.recv_timeout(std::time::Duration::from_secs(3)) { Ok(true) => Ok(()), _ => { - let _ = crate::computer::spawn_reaped(&mut std::process::Command::new("open").arg("x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent")); + let _ = crate::computer::spawn_reaped(std::process::Command::new("open").arg("x-apple.systempreferences:com.apple.preference.security?Privacy_ListenEvent")); Err("macOS 還沒讓 AnyBot 觀看你的操作。我已經打開「系統設定 → 隱私權與安全性 → 輸入監控」:把 AnyBot(或「終端機」)打開,再按一次「開始錄製」。".into()) } } @@ -602,7 +602,7 @@ mod tests { assert!(std::fs::metadata(&v).unwrap().len() > 0); } let demo = Demo { name: "報表".into(), created: String::new(), steps: vec!["點 (1, 2)".into()], video: String::new(), shots: vec![dir.join("frame_00002.jpg").display().to_string()] }; - let (t, ok, img) = view_step(&[demo.clone()], r#"{"demo":"報表","step":1}"#); + let (t, ok, img) = view_step(std::slice::from_ref(&demo), r#"{"demo":"報表","step":1}"#); assert!(ok && img.is_some() && t.contains("第 1 步"), "{t}"); assert!(!view_step(&[demo], r#"{"demo":"報表","step":5}"#).1); let _ = std::fs::remove_dir_all(&dir); diff --git a/src/tools.rs b/src/tools.rs index ce41a4a..e327746 100644 --- a/src/tools.rs +++ b/src/tools.rs @@ -1371,7 +1371,7 @@ mod tests { let secret = std::env::temp_dir().join(format!("anybot-secret-{}", std::process::id())); std::fs::create_dir_all(&secret).unwrap(); std::fs::write(secret.join("k.txt"), "top-secret").unwrap(); - let out = build_command(&format!("cat '{}/k.txt'", secret.display()), &w, &[secret.clone()], "1", "r").output().unwrap(); + let out = build_command(&format!("cat '{}/k.txt'", secret.display()), &w, std::slice::from_ref(&secret), "1", "r").output().unwrap(); assert!(!String::from_utf8_lossy(&out.stdout).contains("top-secret")); let out = build_command(&format!("cat '{}/k.txt'", secret.display()), &w, &[], "1", "r").output().unwrap(); assert!(String::from_utf8_lossy(&out.stdout).contains("top-secret")); @@ -1379,7 +1379,7 @@ mod tests { let att = secret.join("attachments"); std::fs::create_dir_all(&att).unwrap(); std::fs::write(att.join("photo.txt"), "the-photo").unwrap(); - let run = |cmd: String| build_command_sealed(&cmd, &w, &[secret.clone()], &[att.clone()], None, "1", "r").output().unwrap(); + let run = |cmd: String| build_command_sealed(&cmd, &w, std::slice::from_ref(&secret), std::slice::from_ref(&att), None, "1", "r").output().unwrap(); assert!(String::from_utf8_lossy(&run(format!("cat '{}/photo.txt'", att.display())).stdout).contains("the-photo")); assert!(!String::from_utf8_lossy(&run(format!("cat '{}/k.txt'", secret.display())).stdout).contains("top-secret")); run(format!("echo changed > '{}/photo.txt'", att.display())); diff --git a/src/vault.rs b/src/vault.rs index ba92ed8..ea3123d 100644 --- a/src/vault.rs +++ b/src/vault.rs @@ -63,7 +63,7 @@ pub fn list_for_bot(args: &str) -> String { /// "https://accounts.example.com/login?x" → "accounts.example.com" pub fn host_of(url: &str) -> String { let rest = url.split("://").nth(1).unwrap_or(url); - rest.split(['/', '?', '#']).next().unwrap_or("").split('@').last().unwrap_or("").split(':').next().unwrap_or("").to_lowercase() + rest.split(['/', '?', '#']).next().unwrap_or("").split('@').next_back().unwrap_or("").split(':').next().unwrap_or("").to_lowercase() } /// accounts.google.com → [accounts.google.com, google.com] diff --git a/src/vault_store.rs b/src/vault_store.rs index 2d5b100..8da6297 100644 --- a/src/vault_store.rs +++ b/src/vault_store.rs @@ -193,7 +193,7 @@ pub fn new_id() -> String { pub fn host_of(s: &str) -> String { let s = s.trim(); let rest = s.split("://").nth(1).unwrap_or(s); - let h = rest.split(['/', '?', '#']).next().unwrap_or("").split('@').last().unwrap_or("").split(':').next().unwrap_or("").to_lowercase(); + let h = rest.split(['/', '?', '#']).next().unwrap_or("").split('@').next_back().unwrap_or("").split(':').next().unwrap_or("").to_lowercase(); h.strip_prefix("www.").map(String::from).unwrap_or(h) } @@ -237,7 +237,7 @@ pub fn luhn_ok(number: &str) -> bool { } n }).sum(); - sum % 10 == 0 + sum.is_multiple_of(10) } pub fn card_brand(number: &str) -> &'static str { @@ -317,7 +317,7 @@ fn hex(b: &[u8]) -> String { fn unhex(s: &str) -> Option> { let s = s.trim(); - (s.len() % 2 == 0).then_some(())?; + s.len().is_multiple_of(2).then_some(())?; (0..s.len() / 2).map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).ok()).collect() } @@ -482,7 +482,7 @@ fn import_keychain_once() { pub fn list() -> Result, String> { import_keychain_once(); let mut v: Vec = load()?.items.iter().map(Item::public).collect(); - v.sort_by(|a, b| b.updated_ms.cmp(&a.updated_ms)); + v.sort_by_key(|i| std::cmp::Reverse(i.updated_ms)); Ok(v) } diff --git a/src/voice.rs b/src/voice.rs index f8fff6e..95ac78e 100644 --- a/src/voice.rs +++ b/src/voice.rs @@ -163,7 +163,7 @@ impl Recording { } match said.trim() { "denied" => { - let _ = crate::computer::spawn_reaped(&mut Command::new("open").arg("x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone")); + let _ = crate::computer::spawn_reaped(Command::new("open").arg("x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone")); Err("macOS 還沒讓 AnyBot 使用麥克風。我已經打開「系統設定 → 隱私權與安全性 → 麥克風」:把 AnyBot(或「終端機」)打開,再試一次。".into()) } "fail" => Err("找不到可以用的麥克風。".into()), diff --git a/src/web.rs b/src/web.rs index c6019f5..01679b7 100644 --- a/src/web.rs +++ b/src/web.rs @@ -404,7 +404,7 @@ fn explain(err: &str, browser: &str) -> String { format!("macOS 擋下了 AnyBot 控制 {browser}。我已經幫你打開「系統設定 → 隱私權與安全性 → 自動化」:在 AnyBot(或「終端機」)底下打開「{browser}」,然後再叫我試一次。") } else if lower.contains("javascript") { if browser == "Safari" { - format!("Safari 還沒允許讀取網頁內容:請在 Safari 選單「開發 → 允許來自 Apple 事件的 JavaScript」打開(沒有「開發」選單的話,先到 Safari 設定 → 進階 → 勾選「顯示網頁開發者功能」),然後再叫我試一次。") + "Safari 還沒允許讀取網頁內容:請在 Safari 選單「開發 → 允許來自 Apple 事件的 JavaScript」打開(沒有「開發」選單的話,先到 Safari 設定 → 進階 → 勾選「顯示網頁開發者功能」),然後再叫我試一次。".to_string() } else { format!("{browser} 還沒允許讀取網頁內容:請在 {browser} 選單「檢視 → 開發人員 → 允許來自 Apple 事件的 JavaScript」打開,然後再叫我試一次。") } diff --git a/src/wl.rs b/src/wl.rs index e780fc1..ef206b4 100644 --- a/src/wl.rs +++ b/src/wl.rs @@ -362,7 +362,7 @@ pub fn to_rgba(data: &[u8], fmt: wl_shm::Format, w: u32, h: u32, stride: u32, y_ let r = if y_invert { h - 1 - row } else { row }; let (a, b) = ((r as u64 * stride as u64) as usize, ((r as u64 * stride as u64) as usize) + (w as usize) * 4); let line = data.get(a..b)?; - for px in line.chunks_exact(4) { + for px in line.as_chunks::<4>().0 { if bgr { out.extend_from_slice(&[px[2], px[1], px[0], 255]); } else { @@ -614,7 +614,7 @@ impl Dispatch for State { } } top_handle::Event::State { state } => { - let active = state.chunks_exact(4).any(|c| u32::from_ne_bytes([c[0], c[1], c[2], c[3]]) == top_handle::State::Activated as u32); + let active = state.as_chunks::<4>().0.iter().any(|c| u32::from_ne_bytes(*c) == top_handle::State::Activated as u32); if let Some(t) = st.tops.iter_mut().find(|t| t.id == id) { t.active = active; } diff --git a/src/x11.rs b/src/x11.rs index 2252b22..c9e36cf 100644 --- a/src/x11.rs +++ b/src/x11.rs @@ -55,7 +55,7 @@ pub fn capture() -> Result { let lsb = setup.image_byte_order == xproto::ImageOrder::LSB_FIRST; let (w, h) = (x.width as usize, x.height as usize); let mut rgb = Vec::with_capacity(w * h * 3); - for px in reply.data.chunks_exact(4).take(w * h) { + for px in reply.data.as_chunks::<4>().0.iter().take(w * h) { if lsb { rgb.extend_from_slice(&[px[2], px[1], px[0]]); } else { @@ -421,7 +421,6 @@ pub fn record(mut each: impl FnMut(Input), keep_going: impl Fn() -> bool + Send }; let stop = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); let stopper = std::thread::spawn({ - let ctx = ctx; let stop = stop.clone(); move || { // Disabling the context ends the data stream below. @@ -450,7 +449,7 @@ pub fn record(mut each: impl FnMut(Input), keep_going: impl Fn() -> bool + Send if reply.category != 0 { continue; } - for ev in reply.data.chunks_exact(32) { + for ev in reply.data.as_chunks::<32>().0 { let (kind, detail) = (ev[0] & 0x7f, ev[1]); let rx = i16::from_ne_bytes([ev[20], ev[21]]) as f64; let ry = i16::from_ne_bytes([ev[22], ev[23]]) as f64; @@ -541,7 +540,7 @@ mod tests { assert!(available(), "no X display"); let (w, h) = screen_pixels(); let shot = capture().expect("screenshot"); - assert_eq!((shot.width(), shot.height()), (w as u32, h as u32)); + assert_eq!((shot.width(), shot.height()), (w, h)); // Record while clicking and typing through XTEST: the recorder must see both. let got = std::sync::Arc::new(Mutex::new(Vec::new())); let g = got.clone();