AnyBot/src/desk.rs

324 lines
9.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<Kind> = 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<image::RgbImage, String> {
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,
}
}
// ---------- accessibility (the numbered controls) ----------
/// The program a window belongs to, the way agent-desktop names apps (the executable's name): a
/// Snap Firefox's window says "firefox_firefox", its program is "firefox".
pub fn program_of(pid: i32) -> Option<String> {
if pid <= 0 {
return None;
}
let exe = std::fs::read_link(format!("/proc/{pid}/exe")).ok()?;
let name = exe.file_name()?.to_str()?.trim_end_matches(" (deleted)").to_string();
(!name.is_empty()).then_some(name)
}
/// Turn the desktop's accessibility switch on when it is off (what a screen reader does):
/// without it most apps never show their controls, so the bot gets no numbers. True when it was
/// off and is on now; apps already open show theirs once restarted.
pub fn accessibility_switched_on() -> bool {
let Ok(conn) = zbus::blocking::Connection::session() else { return false };
let Ok(p) = zbus::blocking::Proxy::new(&conn, "org.a11y.Bus", "/org/a11y/bus", "org.a11y.Status") else { return false };
if p.get_property::<bool>("IsEnabled").unwrap_or(true) {
return false;
}
p.set_property("IsEnabled", true).is_ok()
}
// ---------- idle and screensaver ----------
fn dbus_call<R: serde::de::DeserializeOwned + zbus::zvariant::Type>(dest: &str, path: &str, iface: &str, method: &str) -> Option<R> {
let conn = zbus::blocking::Connection::session().ok()?;
let reply = conn.call_method(Some(dest), path, Some(iface), method, &()).ok()?;
reply.body().deserialize::<R>().ok()
}
/// Seconds since the last keyboard or mouse input.
pub fn idle_secs() -> Option<f64> {
if !wayland() {
return x11::idle_secs();
}
// GNOME: Mutter's idle monitor (milliseconds).
if let Some(ms) = dbus_call::<u64>("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::<bool>("org.gnome.ScreenSaver", "/org/gnome/ScreenSaver", "org.gnome.ScreenSaver", "GetActive")
.or_else(|| dbus_call::<bool>("org.freedesktop.ScreenSaver", "/ScreenSaver", "org.freedesktop.ScreenSaver", "IsActive"))
.unwrap_or(false)
}