Skip to main content

maa_framework/
toolkit.rs

1//! Device discovery and configuration utilities.
2
3use serde::{Deserialize, Serialize};
4
5use crate::{MaaError, MaaResult, common, sys};
6use std::ffi::{CStr, CString};
7use std::path::{Path, PathBuf};
8use std::sync::Once;
9
10/// Information about a connected ADB device.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct AdbDevice {
13    /// Device display name.
14    pub name: String,
15    /// Path to the ADB executable.
16    pub adb_path: PathBuf,
17    /// Device address (e.g., "127.0.0.1:5555").
18    pub address: String,
19    /// Supported screencap methods (bitflags).
20    pub screencap_methods: u64,
21    /// Supported input methods (bitflags).
22    pub input_methods: u64,
23    /// Device configuration as JSON.
24    pub config: serde_json::Value,
25}
26
27/// Information about a desktop window (Win32).
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct DesktopWindow {
30    /// Window handle (HWND).
31    pub hwnd: usize,
32    /// Window class name.
33    pub class_name: String,
34    /// Window title.
35    pub window_name: String,
36}
37
38/// A gamescope instance discovered from the session.
39///
40/// Combines the display number, PipeWire capture node and libei (EIS) socket of
41/// a single gamescope instance into one model.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct GamescopeInstance {
44    /// Display number (`n` in `gamescope-<n>`).
45    pub display_no: u32,
46    /// PipeWire node ID. Usable as `pw_node_id` in
47    /// [`crate::common::LinuxControllerConfig`]. `0` means no capture node.
48    pub pipewire_node_id: u32,
49    /// EIS socket path. Usable as `eis_socket_path` in
50    /// [`crate::common::LinuxControllerConfig`]. Empty when no EIS socket exists.
51    pub eis_socket_path: String,
52}
53
54/// macOS system permission types used by toolkit helpers.
55#[repr(i32)]
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57pub enum MacOSPermission {
58    /// Screen recording / screen capture permission.
59    ScreenCapture = sys::MaaMacOSPermissionEnum_MaaMacOSPermissionScreenCapture as i32,
60    /// Accessibility permission for input simulation.
61    Accessibility = sys::MaaMacOSPermissionEnum_MaaMacOSPermissionAccessibility as i32,
62}
63
64/// Toolkit utilities for device discovery and configuration.
65pub struct Toolkit;
66
67static AGENT_SERVER_INIT_OPTION_WARNING: Once = Once::new();
68
69impl Toolkit {
70    #[inline]
71    fn unsupported(api: &str) -> MaaError {
72        MaaError::UnsupportedInAgentServer(api.to_string())
73    }
74
75    fn maybe_warn_init_option_in_agent_server() {
76        if std::env::var_os("MAA_RUST_WARN_AGENTSERVER_TOOLKIT_INIT").is_none() {
77            return;
78        }
79
80        AGENT_SERVER_INIT_OPTION_WARNING.call_once(|| {
81            eprintln!(
82                "Warning: Toolkit::init_option is deprecated in AgentServer; only log_dir is applied."
83            );
84        });
85    }
86
87    /// Initialize MAA framework options.
88    ///
89    /// # Arguments
90    /// * `user_path` - Path to user data directory
91    /// * `default_config` - Default configuration JSON string
92    pub fn init_option(user_path: &str, default_config: &str) -> MaaResult<()> {
93        if crate::is_agent_server_context() {
94            let _ = default_config;
95            Self::maybe_warn_init_option_in_agent_server();
96            let log_dir = Path::new(user_path).join("debug");
97            return crate::configure_logging(log_dir.to_string_lossy().as_ref());
98        }
99
100        let c_path = CString::new(user_path)?;
101        let c_config = CString::new(default_config)?;
102        let ret = unsafe { sys::MaaToolkitConfigInitOption(c_path.as_ptr(), c_config.as_ptr()) };
103        common::check_bool(ret)
104    }
105
106    /// Find connected ADB devices.
107    ///
108    /// Scans for all known Android emulators and connected ADB devices.
109    ///
110    /// # Returns
111    /// List of discovered ADB devices with their configurations.
112    pub fn find_adb_devices() -> MaaResult<Vec<AdbDevice>> {
113        if crate::is_agent_server_context() {
114            return Err(Self::unsupported("Toolkit::find_adb_devices"));
115        }
116        Self::find_adb_devices_impl(None)
117    }
118
119    /// Find connected ADB devices using a specific ADB binary.
120    ///
121    /// # Arguments
122    /// * `adb_path` - Path to the ADB binary to use for discovery
123    ///
124    /// # Returns
125    /// List of discovered ADB devices with their configurations.
126    pub fn find_adb_devices_with_adb(adb_path: &str) -> MaaResult<Vec<AdbDevice>> {
127        if crate::is_agent_server_context() {
128            return Err(Self::unsupported("Toolkit::find_adb_devices_with_adb"));
129        }
130        Self::find_adb_devices_impl(Some(adb_path))
131    }
132
133    fn find_adb_devices_impl(specified_adb: Option<&str>) -> MaaResult<Vec<AdbDevice>> {
134        let list = unsafe { sys::MaaToolkitAdbDeviceListCreate() };
135        if list.is_null() {
136            return Err(MaaError::NullPointer);
137        }
138
139        let _guard = AdbDeviceListGuard(list);
140
141        unsafe {
142            let ret = if let Some(adb_path) = specified_adb {
143                let c_path = CString::new(adb_path)?;
144                sys::MaaToolkitAdbDeviceFindSpecified(c_path.as_ptr(), list)
145            } else {
146                sys::MaaToolkitAdbDeviceFind(list)
147            };
148            common::check_bool(ret)?;
149
150            let count = sys::MaaToolkitAdbDeviceListSize(list);
151            let mut devices = Vec::with_capacity(count as usize);
152
153            for i in 0..count {
154                let device_ptr = sys::MaaToolkitAdbDeviceListAt(list, i);
155                if device_ptr.is_null() {
156                    continue;
157                }
158
159                let name = CStr::from_ptr(sys::MaaToolkitAdbDeviceGetName(device_ptr))
160                    .to_string_lossy()
161                    .into_owned();
162
163                let adb_path_str = CStr::from_ptr(sys::MaaToolkitAdbDeviceGetAdbPath(device_ptr))
164                    .to_string_lossy()
165                    .into_owned();
166
167                let address = CStr::from_ptr(sys::MaaToolkitAdbDeviceGetAddress(device_ptr))
168                    .to_string_lossy()
169                    .into_owned();
170
171                let screencap_methods =
172                    sys::MaaToolkitAdbDeviceGetScreencapMethods(device_ptr) as u64;
173                let input_methods = sys::MaaToolkitAdbDeviceGetInputMethods(device_ptr) as u64;
174
175                let config_str =
176                    CStr::from_ptr(sys::MaaToolkitAdbDeviceGetConfig(device_ptr)).to_string_lossy();
177                let config = serde_json::from_str(&config_str).unwrap_or(serde_json::Value::Null);
178
179                devices.push(AdbDevice {
180                    name,
181                    adb_path: PathBuf::from(adb_path_str),
182                    address,
183                    screencap_methods,
184                    input_methods,
185                    config,
186                });
187            }
188            Ok(devices)
189        }
190    }
191
192    /// Find all desktop windows (Win32 only).
193    ///
194    /// # Returns
195    /// List of visible desktop windows.
196    pub fn find_desktop_windows() -> MaaResult<Vec<DesktopWindow>> {
197        if crate::is_agent_server_context() {
198            return Err(Self::unsupported("Toolkit::find_desktop_windows"));
199        }
200
201        let list = unsafe { sys::MaaToolkitDesktopWindowListCreate() };
202        if list.is_null() {
203            return Err(MaaError::NullPointer);
204        }
205
206        let _guard = DesktopWindowListGuard(list);
207
208        unsafe {
209            let ret = sys::MaaToolkitDesktopWindowFindAll(list);
210            common::check_bool(ret)?;
211
212            let count = sys::MaaToolkitDesktopWindowListSize(list);
213            let mut windows = Vec::with_capacity(count as usize);
214
215            for i in 0..count {
216                let win_ptr = sys::MaaToolkitDesktopWindowListAt(list, i);
217                if win_ptr.is_null() {
218                    continue;
219                }
220
221                let hwnd = sys::MaaToolkitDesktopWindowGetHandle(win_ptr) as usize;
222
223                let class_name = CStr::from_ptr(sys::MaaToolkitDesktopWindowGetClassName(win_ptr))
224                    .to_string_lossy()
225                    .into_owned();
226
227                let window_name =
228                    CStr::from_ptr(sys::MaaToolkitDesktopWindowGetWindowName(win_ptr))
229                        .to_string_lossy()
230                        .into_owned();
231
232                windows.push(DesktopWindow {
233                    hwnd,
234                    class_name,
235                    window_name,
236                });
237            }
238            Ok(windows)
239        }
240    }
241
242    /// Check whether the current process has the specified macOS permission.
243    pub fn macos_check_permission(permission: MacOSPermission) -> MaaResult<bool> {
244        if crate::is_agent_server_context() {
245            return Err(Self::unsupported("Toolkit::macos_check_permission"));
246        }
247
248        let ret =
249            unsafe { sys::MaaToolkitMacOSCheckPermission(permission as sys::MaaMacOSPermission) };
250        Ok(ret != 0)
251    }
252
253    /// Request the specified macOS permission from the system.
254    ///
255    /// A successful return means the request API call succeeded. It does not
256    /// necessarily mean the user has already granted the permission.
257    pub fn macos_request_permission(permission: MacOSPermission) -> MaaResult<bool> {
258        if crate::is_agent_server_context() {
259            return Err(Self::unsupported("Toolkit::macos_request_permission"));
260        }
261
262        let ret =
263            unsafe { sys::MaaToolkitMacOSRequestPermission(permission as sys::MaaMacOSPermission) };
264        Ok(ret != 0)
265    }
266
267    /// Open the corresponding macOS settings page for the permission.
268    pub fn macos_reveal_permission_settings(permission: MacOSPermission) -> MaaResult<bool> {
269        if crate::is_agent_server_context() {
270            return Err(Self::unsupported(
271                "Toolkit::macos_reveal_permission_settings",
272            ));
273        }
274
275        let ret = unsafe {
276            sys::MaaToolkitMacOSRevealPermissionSettings(permission as sys::MaaMacOSPermission)
277        };
278        Ok(ret != 0)
279    }
280
281    /// Find gamescope instances on the session.
282    ///
283    /// Each instance bundles a display number, a PipeWire capture node and an
284    /// EIS socket. The `pipewire_node_id` / `eis_socket_path` can be passed to
285    /// [`crate::common::LinuxControllerConfig`] to capture and control a
286    /// gamescope window directly, without going through the ScreenCast portal.
287    ///
288    /// Returns an empty list when gamescope is not running, and on non-Linux
289    /// platforms (where the underlying C API returns no instances).
290    pub fn find_gamescope_instances() -> MaaResult<Vec<GamescopeInstance>> {
291        if crate::is_agent_server_context() {
292            return Err(Self::unsupported("Toolkit::find_gamescope_instances"));
293        }
294
295        let list = unsafe { sys::MaaToolkitGamescopeInstanceListCreate() };
296        if list.is_null() {
297            return Err(MaaError::NullPointer);
298        }
299
300        let _guard = GamescopeInstanceListGuard(list);
301
302        unsafe {
303            common::check_bool(sys::MaaToolkitGamescopeInstanceFindAll(list))?;
304
305            let count = sys::MaaToolkitGamescopeInstanceListSize(list);
306            let mut instances = Vec::with_capacity(count as usize);
307
308            for i in 0..count {
309                let instance_ptr = sys::MaaToolkitGamescopeInstanceListAt(list, i);
310                if instance_ptr.is_null() {
311                    continue;
312                }
313
314                let display_no = sys::MaaToolkitGamescopeInstanceGetDisplayNo(instance_ptr);
315                let pipewire_node_id =
316                    sys::MaaToolkitGamescopeInstanceGetPipeWireNodeId(instance_ptr);
317                let eis_socket_path =
318                    sys::MaaToolkitGamescopeInstanceGetEisSocketPath(instance_ptr);
319                if eis_socket_path.is_null() {
320                    return Err(MaaError::NullPointer);
321                }
322                let eis_socket_path = CStr::from_ptr(eis_socket_path)
323                    .to_string_lossy()
324                    .into_owned();
325
326                instances.push(GamescopeInstance {
327                    display_no,
328                    pipewire_node_id,
329                    eis_socket_path,
330                });
331            }
332            Ok(instances)
333        }
334    }
335}
336
337struct AdbDeviceListGuard(*mut sys::MaaToolkitAdbDeviceList);
338impl Drop for AdbDeviceListGuard {
339    fn drop(&mut self) {
340        unsafe { sys::MaaToolkitAdbDeviceListDestroy(self.0) }
341    }
342}
343
344struct DesktopWindowListGuard(*mut sys::MaaToolkitDesktopWindowList);
345impl Drop for DesktopWindowListGuard {
346    fn drop(&mut self) {
347        unsafe { sys::MaaToolkitDesktopWindowListDestroy(self.0) }
348    }
349}
350
351struct GamescopeInstanceListGuard(*mut sys::MaaToolkitGamescopeInstanceList);
352impl Drop for GamescopeInstanceListGuard {
353    fn drop(&mut self) {
354        unsafe { sys::MaaToolkitGamescopeInstanceListDestroy(self.0) }
355    }
356}
357
358/// XDG Desktop Portal ScreenCast helper (Linux only).
359///
360/// Opens a ScreenCast portal session to obtain a PipeWire stream, whose FD and
361/// node ID can be handed to [`crate::controller::Controller::new_linux`] via
362/// [`crate::common::LinuxControllerConfig`]'s `pw_socket_fd` / `pw_node_id`.
363///
364/// On non-Linux platforms, [`PortalHelper::new`] returns
365/// [`MaaError::NullPointer`] because the underlying C API is unavailable there.
366pub struct PortalHelper {
367    handle: *mut sys::MaaToolkitPortalHelper,
368}
369
370impl PortalHelper {
371    /// Create a new portal helper.
372    pub fn new() -> MaaResult<Self> {
373        let handle = unsafe { sys::MaaToolkitPortalHelperCreate() };
374        if handle.is_null() {
375            return Err(MaaError::NullPointer);
376        }
377        Ok(Self { handle })
378    }
379
380    /// Open the ScreenCast portal stream (create DBus session, select sources,
381    /// and start the stream).
382    pub fn open_stream(&self) -> MaaResult<()> {
383        common::check_bool(unsafe { sys::MaaToolkitPortalHelperOpenStream(self.handle) })
384    }
385
386    /// Whether the portal session is persistent (i.e. can be restored later).
387    pub fn get_persist(&self) -> bool {
388        unsafe { sys::MaaToolkitPortalHelperGetPersist(self.handle) != 0 }
389    }
390
391    /// Set whether the portal session should persist for later restoration.
392    pub fn set_persist(&self, enable: bool) {
393        unsafe { sys::MaaToolkitPortalHelperSetPersist(self.handle, enable as sys::MaaBool) };
394    }
395
396    /// The PipeWire socket FD, or `-1` if the stream has not been opened yet.
397    pub fn get_pipewire_fd(&self) -> i32 {
398        unsafe { sys::MaaToolkitPortalHelperGetPipeWireFD(self.handle) }
399    }
400
401    /// The PipeWire node ID, or `0` if the stream has not been opened yet.
402    pub fn get_pipewire_node_id(&self) -> u32 {
403        unsafe { sys::MaaToolkitPortalHelperGetPipeWireNodeID(self.handle) }
404    }
405
406    /// The restore token used to restore a persistent session.
407    ///
408    /// Returns an empty string when no token is available.
409    pub fn get_restore_token(&self) -> String {
410        let ptr = unsafe { sys::MaaToolkitPortalHelperGetRestoreToken(self.handle) };
411        if ptr.is_null() {
412            return String::new();
413        }
414        unsafe { CStr::from_ptr(ptr) }
415            .to_string_lossy()
416            .into_owned()
417    }
418
419    /// Set the restore token to restore a previous portal session.
420    pub fn set_restore_token(&self, token: &str) -> MaaResult<()> {
421        let c_token = CString::new(token)?;
422        unsafe { sys::MaaToolkitPortalHelperSetRestoreToken(self.handle, c_token.as_ptr()) };
423        Ok(())
424    }
425}
426
427impl Drop for PortalHelper {
428    fn drop(&mut self) {
429        unsafe { sys::MaaToolkitPortalHelperDestroy(self.handle) };
430    }
431}