// Linux: one front for seeing and using the screen, whatever the desktop is. // X11 (Xorg sessions) x11.rs // Wayland, wlroots (sway, Hyprland…) wl.rs – the compositor's own protocols, no dialog // Wayland, GNOME / KDE portal.rs – the desktop asks the person once // The functions match computer.rs's macOS part, so the computer_use tools work the same way. use std::sync::OnceLock; use std::time::Duration; use crate::x11; #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Kind { X11, Wlr, Portal, None, } pub fn wayland() -> bool { std::env::var("WAYLAND_DISPLAY").map(|v| !v.is_empty()).unwrap_or(false) } /// Which way this session works (decided once). pub fn kind() -> Kind { static K: OnceLock = OnceLock::new(); *K.get_or_init(|| { if std::env::var("ANYBOT_DESK").as_deref() == Ok("x11") { return if x11::available() { Kind::X11 } else { Kind::None }; } if wayland() { if crate::wl::available() { Kind::Wlr } else if crate::portal::available() { Kind::Portal } else { Kind::None } } else if x11::available() { Kind::X11 } else { Kind::None } }) } pub fn available() -> bool { kind() != Kind::None } /// A sentence for the bot and the settings page. pub fn describe() -> &'static str { match kind() { Kind::X11 => "Linux(X11):看畫面和操作滑鼠鍵盤都可以用,不需要另外允許。", Kind::Wlr => "Linux(Wayland):看畫面和操作滑鼠鍵盤都可以用,不需要另外允許。", Kind::Portal => "Linux(Wayland):第一次看畫面時,系統會問要不要讓 App 截圖,選「允許」;第一次操作時會跳出「遠端桌面」視窗,打開「允許遠端操作」再按「分享」。", Kind::None if wayland() => "這個 Wayland 桌面沒有提供讓 App 操作畫面的方式(找不到 xdg-desktop-portal 的遠端桌面功能)。請安裝 xdg-desktop-portal-gnome 或 xdg-desktop-portal-kde,或在登入畫面改選 Xorg 工作階段。", Kind::None => "這台 Linux 沒有桌面畫面(找不到顯示器),所以沒辦法看畫面和操作。", } } pub fn capture() -> Result { match kind() { Kind::X11 => x11::capture(), Kind::Wlr => crate::wl::capture(), Kind::Portal => crate::portal::capture(), Kind::None => Err(describe().into()), } } pub fn screen_points() -> (f64, f64) { match kind() { Kind::X11 => x11::screen_points(), Kind::Wlr => crate::wl::screen_points(), Kind::Portal => crate::portal::screen_points(), Kind::None => (0.0, 0.0), } } pub fn screen_pixels() -> (u32, u32) { match kind() { Kind::X11 => x11::screen_pixels(), Kind::Wlr => crate::wl::screen_pixels(), Kind::Portal => crate::portal::screen_pixels(), Kind::None => (0, 0), } } pub fn screen_capture_preflight() -> bool { available() } // ---------- the Wayland primitives, and actions built from them ---------- fn motion(x: f64, y: f64) { match kind() { Kind::Wlr => crate::wl::motion(x, y), Kind::Portal => crate::portal::motion(x, y), _ => {} } } fn button(b: u8, press: bool) { match kind() { Kind::Wlr => crate::wl::button(b, press), Kind::Portal => crate::portal::button(b, press), _ => {} } } fn key(sym: u32, press: bool) { match kind() { Kind::Wlr => crate::wl::key(sym, press), Kind::Portal => crate::portal::key(sym, press), _ => {} } } fn wheel(up: i32, left: i32) { match kind() { Kind::Wlr => crate::wl::wheel(up, left), Kind::Portal => crate::portal::wheel(up, left), _ => {} } } fn sleep_secs(s: f64) { std::thread::sleep(Duration::from_secs_f64(s)); } fn gap() { std::thread::sleep(Duration::from_millis(8)); } fn button_no(button: &str) -> u8 { match button { "right" => 3, "middle" => 2, _ => 1, } } fn with_mods(flags: u64, f: impl FnOnce()) { let mods = x11::modifier_keysyms(flags); for m in &mods { key(*m, true); } if !mods.is_empty() { gap(); } f(); for m in mods.iter().rev() { key(*m, false); } } pub fn move_to(px: f64, py: f64) { if kind() == Kind::X11 { return x11::move_to(px, py); } motion(px, py); } pub fn click(px: f64, py: f64, which: &str, count: i64, flags: u64) { if kind() == Kind::X11 { return x11::click(px, py, which, count, flags); } motion(px, py); gap(); let b = button_no(which); with_mods(flags, || { for n in 1..=count { if n > 1 { sleep_secs(crate::computer::CLICK_REPEAT_GAP_SECS); } button(b, true); sleep_secs(crate::computer::CLICK_HOLD_SECS); button(b, false); } }); } pub fn drag(path: &[(f64, f64)], which: &str, flags: u64) { if kind() == Kind::X11 { return x11::drag(path, which, flags); } let (x0, y0) = path[0]; motion(x0, y0); gap(); let b = button_no(which); with_mods(flags, || { button(b, true); sleep_secs(crate::computer::DRAG_HOLD_SECS); for (x, y) in crate::computer::drag_points(path) { gap(); motion(x, y); } button(b, false); }); } pub fn scroll(at: Option<(f64, f64)>, dy: i32, dx: i32, flags: u64) { if kind() == Kind::X11 { return x11::scroll(at, dy, dx, flags); } if let Some((x, y)) = at { motion(x, y); gap(); } let step = crate::computer::SCROLL_PIXELS_PER_STEP; with_mods(flags, || { // One wheel click at a time, like X11's buttons 4–7. let (vy, vx) = (dy / step, dx / step); for _ in 0..vy.abs() { wheel(vy.signum(), 0); std::thread::sleep(Duration::from_millis(12)); } for _ in 0..vx.abs() { wheel(0, vx.signum()); std::thread::sleep(Duration::from_millis(12)); } }); } pub fn press(code: u16, flags: u64, hold_secs: f64) { if kind() == Kind::X11 { return x11::press(code, flags, hold_secs); } let Some(sym) = x11::keysym_of_mac_code(code) else { return }; with_mods(flags, || { key(sym, true); sleep_secs(hold_secs); key(sym, false); }); } pub fn type_text(text: &str) { match kind() { Kind::X11 => x11::type_text(text), Kind::Wlr => crate::wl::type_text(text), Kind::Portal => crate::portal::type_text(text), Kind::None => {} } } // ---------- windows ---------- /// The app under a point. Wayland doesn't tell apps where other windows are, so only X11 (and /// XWayland windows) can answer. pub fn window_at(px: f64, py: f64) -> Option<(String, i32)> { match kind() { Kind::X11 => x11::window_at(px, py), _ => None, } } pub fn front_app() -> Option<(String, i32)> { match kind() { Kind::X11 => x11::front_app(), Kind::Wlr => crate::wl::front_app(), _ => None, } } // ---------- idle and screensaver ---------- fn dbus_call(dest: &str, path: &str, iface: &str, method: &str) -> Option { let conn = zbus::blocking::Connection::session().ok()?; let reply = conn.call_method(Some(dest), path, Some(iface), method, &()).ok()?; reply.body().deserialize::().ok() } /// Seconds since the last keyboard or mouse input. pub fn idle_secs() -> Option { if !wayland() { return x11::idle_secs(); } // GNOME: Mutter's idle monitor (milliseconds). if let Some(ms) = dbus_call::("org.gnome.Mutter.IdleMonitor", "/org/gnome/Mutter/IdleMonitor/Core", "org.gnome.Mutter.IdleMonitor", "GetIdletime") { return Some(ms as f64 / 1000.0); } // wlroots, KDE and others: ext-idle-notify. crate::wl::idle_secs() } /// The screensaver (or lock screen) is on. pub fn screensaver_on() -> bool { if !wayland() { return x11::screensaver_on(); } // GNOME speaks org.gnome.ScreenSaver.GetActive at /org/gnome/ScreenSaver; the plain // freedesktop one (KDE & co.) is org.freedesktop.ScreenSaver.IsActive at /ScreenSaver — // the old fallback asked for a path and method that never exist, so on KDE it always // answered "screen is free". dbus_call::("org.gnome.ScreenSaver", "/org/gnome/ScreenSaver", "org.gnome.ScreenSaver", "GetActive") .or_else(|| dbus_call::("org.freedesktop.ScreenSaver", "/ScreenSaver", "org.freedesktop.ScreenSaver", "IsActive")) .unwrap_or(false) }