Skip to main content

computer_use/
shortcuts.rs

1//! Run a Shortcuts workflow by name through the `shortcuts` binary.
2//!
3//! `shortcuts run` is the documented entry point. The `shortcuts` binary is
4//! invoked with the workflow name as a single positional argument. A missing
5//! binary surfaces as `execution_failed` rather than an `unsupported` error:
6//! the binary is part of macOS but a process running in a sandboxed context
7//! may still see it as absent.
8
9use std::time::Duration;
10
11use serde::Serialize;
12
13use crate::backend::command::stderr_snippet;
14use crate::error::ComputerError;
15use crate::protocol::APPLE_EVENT_TIMEOUT_SECS;
16
17/// What `shortcuts run` returned.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19pub struct ShortcutsReport {
20    /// Exit status of `shortcuts run`.
21    pub exit_status: i32,
22    /// Trimmed stderr, never the script body.
23    pub stderr: String,
24}
25
26/// Run `shortcuts run <name>` and wait for completion. The process is killed
27/// after [`APPLE_EVENT_TIMEOUT_SECS`] (the same timeout the macOS backend
28/// applies to `osascript`).
29pub fn run(name: &str) -> Result<ShortcutsReport, ComputerError> {
30    let trimmed = name.trim();
31    if trimmed.is_empty() {
32        return Err(ComputerError::InvalidArguments(
33            "shortcuts run requires a workflow name".into(),
34        ));
35    }
36    let timeout = Duration::from_secs(APPLE_EVENT_TIMEOUT_SECS);
37    let output = crate::backend::command::run_timeout("shortcuts", &["run", trimmed], timeout)
38        .map_err(ComputerError::ExecutionFailed)?;
39    let exit_status = output.status.code().unwrap_or(-1);
40    let stderr = stderr_snippet(&output.stderr);
41    if !output.status.success() {
42        return Err(ComputerError::ExecutionFailed(format!(
43            "shortcuts run {trimmed} failed (exit {exit_status}): {stderr}"
44        )));
45    }
46    Ok(ShortcutsReport {
47        exit_status,
48        stderr,
49    })
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55
56    #[test]
57    fn empty_name_is_an_argument_error() {
58        let error = run("").unwrap_err();
59        assert!(matches!(error, ComputerError::InvalidArguments(_)));
60    }
61
62    #[test]
63    fn whitespace_name_is_an_argument_error() {
64        let error = run("   ").unwrap_err();
65        assert!(matches!(error, ComputerError::InvalidArguments(_)));
66    }
67}