Skip to main content

maa_framework/
tasker.rs

1//! Task execution and pipeline management.
2
3use crate::resource::Resource;
4use crate::{common, sys, MaaError, MaaResult};
5use std::ptr::NonNull;
6
7use std::collections::HashMap;
8use std::ffi::c_void;
9use std::sync::Mutex;
10
11/// Task manager for executing pipelines.
12///
13/// Tasker is the central component that coordinates:
14/// - Resource binding (images, models)
15/// - Controller binding (device connection)
16/// - Task execution (pipelines)
17/// - Event handling (callbacks)
18use std::sync::Arc;
19
20struct TaskerInner {
21    handle: NonNull<sys::MaaTasker>,
22    owns_handle: bool,
23    callbacks: Mutex<HashMap<sys::MaaSinkId, usize>>, // Store pointer address
24    event_sinks: Mutex<HashMap<sys::MaaSinkId, usize>>,
25    resource: Mutex<Option<Resource>>,
26    controller: Mutex<Option<crate::controller::Controller>>,
27}
28
29unsafe impl Send for TaskerInner {}
30unsafe impl Sync for TaskerInner {}
31
32/// Task manager for executing pipelines.
33///
34/// Tasker is the central component that coordinates:
35/// - Resource binding (images, models)
36/// - Controller binding (device connection)
37/// - Task execution (pipelines)
38/// - Event handling (callbacks)
39#[derive(Clone)]
40pub struct Tasker {
41    inner: Arc<TaskerInner>,
42}
43
44unsafe impl Send for Tasker {}
45unsafe impl Sync for Tasker {}
46
47impl Tasker {
48    /// Create a new Tasker instance.
49    pub fn new() -> MaaResult<Self> {
50        let handle = unsafe { sys::MaaTaskerCreate() };
51        if let Some(ptr) = NonNull::new(handle) {
52            Ok(Self {
53                inner: Arc::new(TaskerInner {
54                    handle: ptr,
55                    owns_handle: true,
56                    callbacks: Mutex::new(HashMap::new()),
57                    event_sinks: Mutex::new(HashMap::new()),
58                    resource: Mutex::new(None),
59                    controller: Mutex::new(None),
60                }),
61            })
62        } else {
63            Err(MaaError::NullPointer)
64        }
65    }
66
67    /// Create a Tasker from a raw pointer.
68    ///
69    /// # Safety
70    /// The pointer must be valid.
71    /// If `owns` is true, the Tasker will destroy the handle when dropped.
72    pub unsafe fn from_raw(ptr: *mut sys::MaaTasker, owns: bool) -> MaaResult<Self> {
73        if let Some(handle) = NonNull::new(ptr) {
74            Ok(Self {
75                inner: Arc::new(TaskerInner {
76                    handle,
77                    owns_handle: owns,
78                    callbacks: Mutex::new(HashMap::new()),
79                    event_sinks: Mutex::new(HashMap::new()),
80                    resource: Mutex::new(None),
81                    controller: Mutex::new(None),
82                }),
83            })
84        } else {
85            Err(MaaError::NullPointer)
86        }
87    }
88
89    /// Bind a resource to this tasker.
90    pub fn bind_resource(&self, res: &Resource) -> MaaResult<()> {
91        let ret = unsafe { sys::MaaTaskerBindResource(self.inner.handle.as_ptr(), res.raw()) };
92        common::check_bool(ret)?;
93        *self.inner.resource.lock().unwrap() = Some(res.clone());
94        Ok(())
95    }
96
97    /// Bind a controller to this tasker.
98    pub fn bind_controller(&self, ctrl: &crate::controller::Controller) -> MaaResult<()> {
99        let ret = unsafe { sys::MaaTaskerBindController(self.inner.handle.as_ptr(), ctrl.raw()) };
100        common::check_bool(ret)?;
101        *self.inner.controller.lock().unwrap() = Some(ctrl.clone());
102        Ok(())
103    }
104
105    /// Get recognition details by ID.
106    pub fn get_recognition_detail(
107        &self,
108        reco_id: crate::common::MaaId,
109    ) -> MaaResult<Option<crate::common::RecognitionDetail>> {
110        Self::fetch_recognition_detail(self.inner.handle.as_ptr(), reco_id)
111    }
112
113    /// Get action details by ID.
114    pub fn get_action_detail(
115        &self,
116        act_id: crate::common::MaaId,
117    ) -> MaaResult<Option<crate::common::ActionDetail>> {
118        Self::fetch_action_detail(self.inner.handle.as_ptr(), act_id)
119    }
120
121    /// Get node details by ID.
122    pub fn get_node_detail(
123        &self,
124        node_id: crate::common::MaaId,
125    ) -> MaaResult<Option<crate::common::NodeDetail>> {
126        Self::fetch_node_detail(self.inner.handle.as_ptr(), node_id)
127    }
128
129    /// Get task details by ID.
130    pub fn get_task_detail(
131        &self,
132        task_id: crate::common::MaaId,
133    ) -> MaaResult<Option<crate::common::TaskDetail>> {
134        Self::fetch_task_detail(self.inner.handle.as_ptr(), task_id)
135    }
136
137    // Static helpers for fetching details (used by both methods and jobs)
138
139    pub(crate) fn fetch_recognition_detail(
140        handle: *mut sys::MaaTasker,
141        reco_id: crate::common::MaaId,
142    ) -> MaaResult<Option<crate::common::RecognitionDetail>> {
143        let node_name = crate::buffer::MaaStringBuffer::new()?;
144        let algorithm = crate::buffer::MaaStringBuffer::new()?;
145        let mut hit = 0;
146        let mut box_rect = sys::MaaRect {
147            x: 0,
148            y: 0,
149            width: 0,
150            height: 0,
151        };
152        let detail = crate::buffer::MaaStringBuffer::new()?;
153        let raw = crate::buffer::MaaImageBuffer::new()?;
154        let draws = crate::buffer::MaaImageListBuffer::new()?;
155
156        let ret = unsafe {
157            sys::MaaTaskerGetRecognitionDetail(
158                handle,
159                reco_id,
160                node_name.raw(),
161                algorithm.raw(),
162                &mut hit,
163                &mut box_rect,
164                detail.raw(),
165                raw.raw(),
166                draws.raw(),
167            )
168        };
169
170        if ret == 0 {
171            return Ok(None);
172        }
173
174        let algorithm_str = algorithm.as_str().to_string();
175        let algorithm_enum = crate::common::AlgorithmEnum::from(algorithm_str);
176        let detail_val: serde_json::Value =
177            serde_json::from_str(detail.as_str()).unwrap_or(serde_json::Value::Null);
178
179        let mut sub_details = Vec::new();
180
181        if matches!(
182            algorithm_enum,
183            crate::common::AlgorithmEnum::And | crate::common::AlgorithmEnum::Or
184        ) {
185            // Logic A: Recursive parsing for And/Or
186            if let Some(arr) = detail_val.as_array() {
187                for item in arr {
188                    if let Some(sub_id) = item.get("reco_id").and_then(|v| v.as_i64()) {
189                        if let Ok(Some(sub)) = Self::fetch_recognition_detail(handle, sub_id) {
190                            sub_details.push(sub);
191                        }
192                    }
193                }
194            }
195        }
196
197        Ok(Some(crate::common::RecognitionDetail {
198            node_name: node_name.as_str().to_string(),
199            algorithm: algorithm_enum,
200            hit: hit != 0,
201            box_rect: crate::common::Rect::from(box_rect),
202            detail: detail_val,
203            raw_image: raw.to_vec(),
204            draw_images: draws.to_vec_of_vec(),
205            sub_details,
206        }))
207    }
208
209    pub(crate) fn fetch_action_detail(
210        handle: *mut sys::MaaTasker,
211        act_id: crate::common::MaaId,
212    ) -> MaaResult<Option<crate::common::ActionDetail>> {
213        let node_name = crate::buffer::MaaStringBuffer::new()?;
214        let action = crate::buffer::MaaStringBuffer::new()?;
215        let mut result_box = sys::MaaRect {
216            x: 0,
217            y: 0,
218            width: 0,
219            height: 0,
220        };
221        let mut success = 0;
222        let detail = crate::buffer::MaaStringBuffer::new()?;
223
224        let ret = unsafe {
225            sys::MaaTaskerGetActionDetail(
226                handle,
227                act_id,
228                node_name.raw(),
229                action.raw(),
230                &mut result_box,
231                &mut success,
232                detail.raw(),
233            )
234        };
235
236        if ret == 0 {
237            return Ok(None);
238        }
239
240        Ok(Some(crate::common::ActionDetail {
241            node_name: node_name.as_str().to_string(),
242            action: crate::common::ActionEnum::from(action.as_str().to_string()),
243            box_rect: crate::common::Rect::from(result_box),
244            success: success != 0,
245            detail: serde_json::from_str(detail.as_str()).unwrap_or(serde_json::Value::Null),
246        }))
247    }
248
249    pub(crate) fn fetch_node_detail(
250        handle: *mut sys::MaaTasker,
251        node_id: crate::common::MaaId,
252    ) -> MaaResult<Option<crate::common::NodeDetail>> {
253        let node_name = crate::buffer::MaaStringBuffer::new()?;
254        let mut reco_id = 0;
255        let mut act_id = 0;
256        let mut completed = 0;
257
258        let ret = unsafe {
259            sys::MaaTaskerGetNodeDetail(
260                handle,
261                node_id,
262                node_name.raw(),
263                &mut reco_id,
264                &mut act_id,
265                &mut completed,
266            )
267        };
268
269        if ret == 0 {
270            return Ok(None);
271        }
272
273        // Logic A/B: Hydration implies we might want reco/act details in NodeDetail?
274        // common::NodeDetail only has ids. Python wraps calls.
275
276        let recognition = if reco_id > 0 {
277            Self::fetch_recognition_detail(handle, reco_id)?
278        } else {
279            None
280        };
281
282        let action = if act_id > 0 {
283            Self::fetch_action_detail(handle, act_id)?
284        } else {
285            None
286        };
287
288        Ok(Some(crate::common::NodeDetail {
289            node_name: node_name.as_str().to_string(),
290            reco_id,
291            act_id,
292            completed: completed != 0,
293            recognition,
294            action,
295        }))
296    }
297
298    pub(crate) fn fetch_task_detail(
299        handle: *mut sys::MaaTasker,
300        task_id: crate::common::MaaId,
301    ) -> MaaResult<Option<crate::common::TaskDetail>> {
302        let entry = crate::buffer::MaaStringBuffer::new()?;
303        let mut node_id_list_size: sys::MaaSize = 0;
304        let mut status: sys::MaaStatus = 0;
305
306        let ret = unsafe {
307            sys::MaaTaskerGetTaskDetail(
308                handle,
309                task_id,
310                entry.raw(),
311                std::ptr::null_mut(),
312                &mut node_id_list_size,
313                &mut status,
314            )
315        };
316
317        if ret == 0 {
318            return Ok(None);
319        }
320
321        let mut node_id_list = vec![0; node_id_list_size as usize];
322        let ret = unsafe {
323            sys::MaaTaskerGetTaskDetail(
324                handle,
325                task_id,
326                entry.raw(),
327                node_id_list.as_mut_ptr(),
328                &mut node_id_list_size,
329                &mut status,
330            )
331        };
332
333        if ret == 0 {
334            return Ok(None);
335        }
336
337        // Logic B: Hydrate nodes
338        let mut nodes = Vec::with_capacity(node_id_list.len());
339        for &nid in &node_id_list {
340            nodes.push(Self::fetch_node_detail(handle, nid)?);
341        }
342
343        Ok(Some(crate::common::TaskDetail {
344            entry: entry.as_str().to_string(),
345            node_id_list,
346            status: crate::common::MaaStatus(status as i32),
347            nodes,
348        }))
349    }
350
351    /// Post a task for execution.
352    ///
353    /// # Arguments
354    /// * `entry` - The task entry point name
355    /// * `pipeline_override` - JSON string for parameter overrides
356    ///
357    /// # Returns
358    /// A `TaskJob` that can be used to wait for completion and get results.
359    pub fn post_task(
360        &self,
361        entry: &str,
362        pipeline_override: &str,
363    ) -> MaaResult<crate::job::TaskJob<'static, crate::common::TaskDetail>> {
364        let c_entry = std::ffi::CString::new(entry)?;
365        let c_pipeline = std::ffi::CString::new(pipeline_override)?;
366        let id = unsafe {
367            sys::MaaTaskerPostTask(
368                self.inner.handle.as_ptr(),
369                c_entry.as_ptr(),
370                c_pipeline.as_ptr(),
371            )
372        };
373
374        let inner = self.inner.clone();
375        let status_fn: crate::job::StatusFn = Box::new(move |job_id| {
376            crate::common::MaaStatus(unsafe { sys::MaaTaskerStatus(inner.handle.as_ptr(), job_id) })
377        });
378
379        let inner = self.inner.clone();
380        let wait_fn: crate::job::WaitFn = Box::new(move |job_id| {
381            crate::common::MaaStatus(unsafe { sys::MaaTaskerWait(inner.handle.as_ptr(), job_id) })
382        });
383
384        let inner = self.inner.clone();
385        let get_fn =
386            move |task_id: crate::common::MaaId| -> MaaResult<Option<crate::common::TaskDetail>> {
387                Tasker::fetch_task_detail(inner.handle.as_ptr(), task_id)
388            };
389
390        // Prepare override function for TaskJob
391        let inner = self.inner.clone();
392        let override_fn: crate::job::OverridePipelineFn = Box::new(move |job_id, pipeline| {
393            let c_pipeline = std::ffi::CString::new(pipeline)?;
394            let ret = unsafe {
395                sys::MaaTaskerOverridePipeline(inner.handle.as_ptr(), job_id, c_pipeline.as_ptr())
396            };
397            Ok(ret != 0)
398        });
399
400        Ok(crate::job::TaskJob::new(
401            crate::job::JobWithResult::new(id, status_fn, wait_fn, get_fn),
402            override_fn,
403        ))
404    }
405
406    /// Post a task with JSON pipeline override.
407    ///
408    /// Convenience method that accepts `serde_json::Value` for pipeline overrides.
409    ///
410    /// # Example
411    /// ```ignore
412    /// use serde_json::json;
413    /// let job = tasker.post_task_json("StartTask", &json!({
414    ///     "StartTask": { "next": ["SecondTask"] }
415    /// }))?;
416    /// ```
417    pub fn post_task_json(
418        &self,
419        entry: &str,
420        pipeline_override: &serde_json::Value,
421    ) -> MaaResult<crate::job::TaskJob<'static, crate::common::TaskDetail>> {
422        self.post_task(entry, &pipeline_override.to_string())
423    }
424
425    /// Check if the tasker has been initialized (resource and controller bound).
426    pub fn inited(&self) -> bool {
427        unsafe { sys::MaaTaskerInited(self.inner.handle.as_ptr()) != 0 }
428    }
429
430    /// Get the underlying raw tasker handle.
431    #[inline]
432    pub fn raw(&self) -> *mut sys::MaaTasker {
433        self.inner.handle.as_ptr()
434    }
435
436    /// Add a tasker event sink callback.
437    ///
438    /// This registers a callback that will be invoked for all tasker events
439    /// including task start, task completion, and status changes.
440    ///
441    /// # Arguments
442    /// * `callback` - Closure that receives (message, detail_json) for each event
443    ///
444    /// # Returns
445    /// Sink ID for later removal via `remove_sink()`
446    pub fn add_sink<F>(&self, callback: F) -> MaaResult<sys::MaaSinkId>
447    where
448        F: Fn(&str, &str) + Send + Sync + 'static,
449    {
450        let (cb, arg) = crate::callback::EventCallback::new(callback);
451        let id = unsafe { sys::MaaTaskerAddSink(self.inner.handle.as_ptr(), cb, arg) };
452        if id > 0 {
453            self.inner
454                .callbacks
455                .lock()
456                .unwrap()
457                .insert(id, arg as usize);
458            Ok(id)
459        } else {
460            unsafe { crate::callback::EventCallback::drop_callback(arg) };
461            Err(MaaError::FrameworkError(0))
462        }
463    }
464
465    /// Register a strongly-typed event sink.
466    ///
467    /// This method registers an implementation of the [`EventSink`](crate::event_sink::EventSink) trait
468    /// to receive structured notifications from this tasker.
469    ///
470    /// # Arguments
471    /// * `sink` - The event sink implementation (must be boxed).
472    ///
473    /// # Returns
474    /// A `MaaSinkId` which can be used to manually remove the sink later via [`remove_sink`](Self::remove_sink).
475    /// The sink will be automatically unregistered and dropped when the `Tasker` is dropped.
476    pub fn add_event_sink(
477        &self,
478        sink: Box<dyn crate::event_sink::EventSink>,
479    ) -> MaaResult<sys::MaaSinkId> {
480        let handle_id = self.inner.handle.as_ptr() as crate::common::MaaId;
481        let (cb, arg) = crate::callback::EventCallback::new_sink(handle_id, sink);
482        let id = unsafe { sys::MaaTaskerAddSink(self.inner.handle.as_ptr(), cb, arg) };
483        if id > 0 {
484            self.inner
485                .event_sinks
486                .lock()
487                .unwrap()
488                .insert(id, arg as usize);
489            Ok(id)
490        } else {
491            unsafe { crate::callback::EventCallback::drop_sink(arg) };
492            Err(MaaError::FrameworkError(0))
493        }
494    }
495
496    /// Remove a tasker sink by ID.
497    ///
498    /// # Arguments
499    /// * `sink_id` - ID returned from `add_sink()`
500    pub fn remove_sink(&self, sink_id: sys::MaaSinkId) {
501        unsafe { sys::MaaTaskerRemoveSink(self.inner.handle.as_ptr(), sink_id) };
502        if let Some(ptr) = self.inner.callbacks.lock().unwrap().remove(&sink_id) {
503            unsafe { crate::callback::EventCallback::drop_callback(ptr as *mut c_void) };
504        } else if let Some(ptr) = self.inner.event_sinks.lock().unwrap().remove(&sink_id) {
505            unsafe { crate::callback::EventCallback::drop_sink(ptr as *mut c_void) };
506        }
507    }
508
509    /// Clear all tasker sinks.
510    pub fn clear_sinks(&self) {
511        unsafe { sys::MaaTaskerClearSinks(self.inner.handle.as_ptr()) };
512        let mut callbacks = self.inner.callbacks.lock().unwrap();
513        for (_, ptr) in callbacks.drain() {
514            unsafe { crate::callback::EventCallback::drop_callback(ptr as *mut c_void) };
515        }
516        let mut event_sinks = self.inner.event_sinks.lock().unwrap();
517        for (_, ptr) in event_sinks.drain() {
518            unsafe { crate::callback::EventCallback::drop_sink(ptr as *mut c_void) };
519        }
520    }
521
522    #[deprecated(since = "0.5.1", note = "Use add_sink() instead")]
523    pub fn register_callback<F>(&self, callback: F) -> MaaResult<crate::common::MaaId>
524    where
525        F: Fn(&str, &str) + Send + Sync + 'static,
526    {
527        self.add_sink(callback)
528    }
529
530    /// Request the tasker to stop all running tasks.
531    pub fn post_stop(&self) -> MaaResult<crate::common::MaaId> {
532        unsafe {
533            let id = sys::MaaTaskerPostStop(self.inner.handle.as_ptr());
534            Ok(id)
535        }
536    }
537
538    /// Check if the tasker is currently running.
539    pub fn is_running(&self) -> bool {
540        unsafe { sys::MaaTaskerRunning(self.inner.handle.as_ptr()) != 0 }
541    }
542
543    /// Check if the tasker is currently running (alias for `is_running`).
544    pub fn running(&self) -> bool {
545        self.is_running()
546    }
547
548    /// Check if the tasker is currently stopping.
549    pub fn stopping(&self) -> bool {
550        unsafe { sys::MaaTaskerStopping(self.inner.handle.as_ptr()) != 0 }
551    }
552
553    // === Context Sink ===
554
555    /// Add a context event sink callback.
556    /// Returns a sink ID for later removal.
557    pub fn add_context_sink<F>(&self, callback: F) -> MaaResult<sys::MaaSinkId>
558    where
559        F: Fn(&str, &str) + Send + Sync + 'static,
560    {
561        let (cb, arg) = crate::callback::EventCallback::new(callback);
562        let id = unsafe { sys::MaaTaskerAddContextSink(self.inner.handle.as_ptr(), cb, arg) };
563        if id > 0 {
564            self.inner
565                .callbacks
566                .lock()
567                .unwrap()
568                .insert(id, arg as usize);
569            Ok(id)
570        } else {
571            unsafe { crate::callback::EventCallback::drop_callback(arg) };
572            Err(MaaError::FrameworkError(0))
573        }
574    }
575
576    /// Register a strongly-typed context event sink.
577    ///
578    /// This receives detailed execution events like Node.Recognition, Node.Action, etc.
579    ///
580    /// # Arguments
581    /// * `sink` - The event sink implementation (must be boxed).
582    ///
583    /// # Returns
584    /// A `MaaSinkId` which can be used to manually remove the sink later via [`remove_context_sink`](Self::remove_context_sink).
585    pub fn add_context_event_sink(
586        &self,
587        sink: Box<dyn crate::event_sink::EventSink>,
588    ) -> MaaResult<sys::MaaSinkId> {
589        let handle_id = self.inner.handle.as_ptr() as crate::common::MaaId;
590        let (cb, arg) = crate::callback::EventCallback::new_sink(handle_id, sink);
591        let id = unsafe { sys::MaaTaskerAddContextSink(self.inner.handle.as_ptr(), cb, arg) };
592        if id > 0 {
593            self.inner
594                .event_sinks
595                .lock()
596                .unwrap()
597                .insert(id, arg as usize);
598            Ok(id)
599        } else {
600            unsafe { crate::callback::EventCallback::drop_sink(arg) };
601            Err(MaaError::FrameworkError(0))
602        }
603    }
604
605    /// Remove a context sink by ID.
606    pub fn remove_context_sink(&self, sink_id: sys::MaaSinkId) {
607        unsafe { sys::MaaTaskerRemoveContextSink(self.inner.handle.as_ptr(), sink_id) };
608        if let Some(ptr) = self.inner.callbacks.lock().unwrap().remove(&sink_id) {
609            unsafe { crate::callback::EventCallback::drop_callback(ptr as *mut c_void) };
610        } else if let Some(ptr) = self.inner.event_sinks.lock().unwrap().remove(&sink_id) {
611            unsafe { crate::callback::EventCallback::drop_sink(ptr as *mut c_void) };
612        }
613    }
614
615    /// Clear all context sinks.
616    pub fn clear_context_sinks(&self) {
617        unsafe { sys::MaaTaskerClearContextSinks(self.inner.handle.as_ptr()) };
618        // Note: callbacks registered via add_context_sink will be cleaned up
619    }
620
621    /// Clear all cached data.
622    pub fn clear_cache(&self) -> MaaResult<()> {
623        let ret = unsafe { sys::MaaTaskerClearCache(self.inner.handle.as_ptr()) };
624        crate::common::check_bool(ret)
625    }
626
627    /// Override pipeline configuration for a specific task.
628    ///
629    /// # Arguments
630    /// * `task_id` - The ID of the task to update.
631    /// * `pipeline_override` - The JSON string containing the new configuration.
632    pub fn override_pipeline(
633        &self,
634        task_id: crate::common::MaaId,
635        pipeline_override: &str,
636    ) -> MaaResult<bool> {
637        let c_pipeline = std::ffi::CString::new(pipeline_override)?;
638        let ret = unsafe {
639            sys::MaaTaskerOverridePipeline(self.inner.handle.as_ptr(), task_id, c_pipeline.as_ptr())
640        };
641        Ok(ret != 0)
642    }
643
644    /// Get the latest node ID for a given node name.
645    pub fn get_latest_node(&self, node_name: &str) -> MaaResult<Option<crate::common::MaaId>> {
646        let c_name = std::ffi::CString::new(node_name)?;
647        let mut node_id: crate::common::MaaId = 0;
648        let ret = unsafe {
649            sys::MaaTaskerGetLatestNode(self.inner.handle.as_ptr(), c_name.as_ptr(), &mut node_id)
650        };
651        if ret != 0 && node_id != 0 {
652            Ok(Some(node_id))
653        } else {
654            Ok(None)
655        }
656    }
657
658    /// Convenience method to bind both resource and controller at once.
659    pub fn bind(
660        &self,
661        resource: &Resource,
662        controller: &crate::controller::Controller,
663    ) -> MaaResult<()> {
664        self.bind_resource(resource)?;
665        self.bind_controller(controller)
666    }
667
668    /// Get a borrowed view of the bound resource.
669    ///
670    /// Returns `None` if no resource is bound.
671    ///
672    /// # Example
673    /// ```ignore
674    /// if let Some(res) = tasker.resource() {
675    ///     println!("Loaded: {}", res.loaded());
676    /// }
677    /// ```
678    pub fn resource(&self) -> Option<crate::resource::ResourceRef<'_>> {
679        let ptr = unsafe { sys::MaaTaskerGetResource(self.inner.handle.as_ptr()) };
680        crate::resource::ResourceRef::from_ptr(ptr)
681    }
682
683    /// Get a borrowed view of the bound controller.
684    ///
685    /// Returns `None` if no controller is bound.
686    ///
687    /// # Example
688    /// ```ignore
689    /// if let Some(ctrl) = tasker.controller() {
690    ///     println!("Connected: {}", ctrl.connected());
691    /// }
692    /// ```
693    pub fn controller(&self) -> Option<crate::controller::ControllerRef<'_>> {
694        let ptr = unsafe { sys::MaaTaskerGetController(self.inner.handle.as_ptr()) };
695        crate::controller::ControllerRef::from_ptr(ptr)
696    }
697
698    /// Get the bound resource handle (raw pointer).
699    ///
700    /// Returns the raw pointer to the resource. The caller should not destroy this handle.
701    pub fn resource_handle(&self) -> *mut sys::MaaResource {
702        unsafe { sys::MaaTaskerGetResource(self.inner.handle.as_ptr()) }
703    }
704
705    /// Get the bound controller handle (raw pointer).
706    ///
707    /// Returns the raw pointer to the controller. The caller should not destroy this handle.
708    pub fn controller_handle(&self) -> *mut sys::MaaController {
709        unsafe { sys::MaaTaskerGetController(self.inner.handle.as_ptr()) }
710    }
711
712    /// Post a recognition task directly without executing through a pipeline.
713    ///
714    /// # Arguments
715    /// * `reco_type` - Recognition type (e.g., "TemplateMatch", "OCR")
716    /// * `reco_param` - Recognition parameters as JSON string
717    /// * `image` - The image to perform recognition on
718    pub fn post_recognition(
719        &self,
720        reco_type: &str,
721        reco_param: &str,
722        image: &crate::buffer::MaaImageBuffer,
723    ) -> MaaResult<crate::job::TaskJob<'static, crate::common::RecognitionDetail>> {
724        let c_type = std::ffi::CString::new(reco_type)?;
725        let c_param = std::ffi::CString::new(reco_param)?;
726        let id = unsafe {
727            sys::MaaTaskerPostRecognition(
728                self.inner.handle.as_ptr(),
729                c_type.as_ptr(),
730                c_param.as_ptr(),
731                image.raw(),
732            )
733        };
734
735        let inner = self.inner.clone();
736        let status_fn: crate::job::StatusFn = Box::new(move |job_id| {
737            common::MaaStatus(unsafe { sys::MaaTaskerStatus(inner.handle.as_ptr(), job_id) })
738        });
739
740        let inner = self.inner.clone();
741        let wait_fn: crate::job::WaitFn = Box::new(move |job_id| {
742            common::MaaStatus(unsafe { sys::MaaTaskerWait(inner.handle.as_ptr(), job_id) })
743        });
744
745        let inner = self.inner.clone();
746        let get_fn = move |reco_id: common::MaaId| -> MaaResult<Option<common::RecognitionDetail>> {
747            Tasker::fetch_recognition_detail(inner.handle.as_ptr(), reco_id)
748        };
749
750        // Prepare override function for TaskJob
751        let inner = self.inner.clone();
752        let override_fn: crate::job::OverridePipelineFn = Box::new(move |job_id, pipeline| {
753            let c_pipeline = std::ffi::CString::new(pipeline)?;
754            let ret = unsafe {
755                sys::MaaTaskerOverridePipeline(inner.handle.as_ptr(), job_id, c_pipeline.as_ptr())
756            };
757            Ok(ret != 0)
758        });
759
760        Ok(crate::job::TaskJob::new(
761            crate::job::JobWithResult::new(id, status_fn, wait_fn, get_fn),
762            override_fn,
763        ))
764    }
765
766    /// Post an action task directly without executing through a pipeline.
767    ///
768    /// # Arguments
769    /// * `action_type` - Action type (e.g., "Click", "Swipe")  
770    /// * `action_param` - Action parameters as JSON string
771    /// * `box_rect` - The target rectangle for the action
772    /// * `reco_detail` - Recognition detail from previous recognition (can be empty)
773    pub fn post_action(
774        &self,
775        action_type: &str,
776        action_param: &str,
777        box_rect: &common::Rect,
778        reco_detail: &str,
779    ) -> MaaResult<crate::job::TaskJob<'static, crate::common::ActionDetail>> {
780        let c_type = std::ffi::CString::new(action_type)?;
781        let c_param = std::ffi::CString::new(action_param)?;
782        let c_detail = std::ffi::CString::new(reco_detail)?;
783        let maa_rect = sys::MaaRect {
784            x: box_rect.x,
785            y: box_rect.y,
786            width: box_rect.width,
787            height: box_rect.height,
788        };
789
790        let id = unsafe {
791            sys::MaaTaskerPostAction(
792                self.inner.handle.as_ptr(),
793                c_type.as_ptr(),
794                c_param.as_ptr(),
795                &maa_rect,
796                c_detail.as_ptr(),
797            )
798        };
799
800        let inner = self.inner.clone();
801        let status_fn: crate::job::StatusFn = Box::new(move |job_id| {
802            common::MaaStatus(unsafe { sys::MaaTaskerStatus(inner.handle.as_ptr(), job_id) })
803        });
804
805        let inner = self.inner.clone();
806        let wait_fn: crate::job::WaitFn = Box::new(move |job_id| {
807            common::MaaStatus(unsafe { sys::MaaTaskerWait(inner.handle.as_ptr(), job_id) })
808        });
809
810        let inner = self.inner.clone();
811        let get_fn = move |act_id: common::MaaId| -> MaaResult<Option<common::ActionDetail>> {
812            Tasker::fetch_action_detail(inner.handle.as_ptr(), act_id)
813        };
814
815        // Prepare override function for TaskJob
816        let inner = self.inner.clone();
817        let override_fn: crate::job::OverridePipelineFn = Box::new(move |job_id, pipeline| {
818            let c_pipeline = std::ffi::CString::new(pipeline)?;
819            let ret = unsafe {
820                sys::MaaTaskerOverridePipeline(inner.handle.as_ptr(), job_id, c_pipeline.as_ptr())
821            };
822            Ok(ret != 0)
823        });
824
825        Ok(crate::job::TaskJob::new(
826            crate::job::JobWithResult::new(id, status_fn, wait_fn, get_fn),
827            override_fn,
828        ))
829    }
830
831    // === Global Methods ===
832
833    /// Set the global log directory.
834    pub fn set_log_dir<P: AsRef<std::path::Path>>(path: P) -> MaaResult<()> {
835        let path_str = path.as_ref().to_string_lossy();
836        let c_path = std::ffi::CString::new(path_str.as_ref())?;
837        let ret = unsafe {
838            sys::MaaGlobalSetOption(
839                sys::MaaGlobalOptionEnum_MaaGlobalOption_LogDir as i32,
840                c_path.as_ptr() as *mut _,
841                c_path.as_bytes().len() as u64,
842            )
843        };
844        common::check_bool(ret)
845    }
846
847    /// Set whether to save debug drawings.
848    pub fn set_save_draw(save: bool) -> MaaResult<()> {
849        let val = if save { 1 } else { 0 };
850        let ret = unsafe {
851            sys::MaaGlobalSetOption(
852                sys::MaaGlobalOptionEnum_MaaGlobalOption_SaveDraw as i32,
853                &val as *const _ as *mut _,
854                std::mem::size_of_val(&val) as u64,
855            )
856        };
857        common::check_bool(ret)
858    }
859
860    /// Set the stdout logging level.
861    pub fn set_stdout_level(level: sys::MaaLoggingLevel) -> MaaResult<()> {
862        let ret = unsafe {
863            sys::MaaGlobalSetOption(
864                sys::MaaGlobalOptionEnum_MaaGlobalOption_StdoutLevel as i32,
865                &level as *const _ as *mut _,
866                std::mem::size_of_val(&level) as u64,
867            )
868        };
869        common::check_bool(ret)
870    }
871
872    /// Enable or disable debug mode (raw image capture, etc).
873    pub fn set_debug_mode(debug: bool) -> MaaResult<()> {
874        let val = if debug { 1 } else { 0 };
875        let ret = unsafe {
876            sys::MaaGlobalSetOption(
877                sys::MaaGlobalOptionEnum_MaaGlobalOption_DebugMode as i32,
878                &val as *const _ as *mut _,
879                std::mem::size_of_val(&val) as u64,
880            )
881        };
882        common::check_bool(ret)
883    }
884
885    /// Set whether to save screenshots on error.
886    pub fn set_save_on_error(save: bool) -> MaaResult<()> {
887        let val = if save { 1 } else { 0 };
888        let ret = unsafe {
889            sys::MaaGlobalSetOption(
890                sys::MaaGlobalOptionEnum_MaaGlobalOption_SaveOnError as i32,
891                &val as *const _ as *mut _,
892                std::mem::size_of_val(&val) as u64,
893            )
894        };
895        common::check_bool(ret)
896    }
897
898    /// Set the JPEG quality for debug drawings (0-100).
899    pub fn set_draw_quality(quality: i32) -> MaaResult<()> {
900        let ret = unsafe {
901            sys::MaaGlobalSetOption(
902                sys::MaaGlobalOptionEnum_MaaGlobalOption_DrawQuality as i32,
903                &quality as *const _ as *mut _,
904                std::mem::size_of_val(&quality) as u64,
905            )
906        };
907        common::check_bool(ret)
908    }
909
910    /// Set the limit for recognition image cache.
911    pub fn set_reco_image_cache_limit(limit: usize) -> MaaResult<()> {
912        let ret = unsafe {
913            sys::MaaGlobalSetOption(
914                sys::MaaGlobalOptionEnum_MaaGlobalOption_RecoImageCacheLimit as i32,
915                &limit as *const _ as *mut _,
916                std::mem::size_of_val(&limit) as u64,
917            )
918        };
919        common::check_bool(ret)
920    }
921
922    /// Load a plugin from the specified path.
923    pub fn load_plugin<P: AsRef<std::path::Path>>(path: P) -> MaaResult<()> {
924        let path_str = path.as_ref().to_string_lossy();
925        let c_path = std::ffi::CString::new(path_str.as_ref())?;
926        let ret = unsafe { sys::MaaGlobalLoadPlugin(c_path.as_ptr()) };
927        common::check_bool(ret)
928    }
929}
930
931impl Drop for TaskerInner {
932    fn drop(&mut self) {
933        unsafe {
934            sys::MaaTaskerClearSinks(self.handle.as_ptr());
935            let mut callbacks = self.callbacks.lock().unwrap();
936            for (_, ptr) in callbacks.drain() {
937                crate::callback::EventCallback::drop_callback(ptr as *mut c_void);
938            }
939            let mut event_sinks = self.event_sinks.lock().unwrap();
940            for (_, ptr) in event_sinks.drain() {
941                crate::callback::EventCallback::drop_sink(ptr as *mut c_void);
942            }
943            if self.owns_handle {
944                sys::MaaTaskerDestroy(self.handle.as_ptr())
945            }
946        }
947    }
948}