cueward-adapter-macos 0.5.0

macOS adapter for Cueward with Safari, Notes, Messages, Reminders, Calendar, Screenshot, Clipboard, and OCR integrations.
use serde_json::{Map, Value, json};
use uuid::Uuid;

use cueward_core::{ShortcutAction, ShortcutReference};

use crate::MacosError;

fn new_uuid() -> String {
    Uuid::new_v4().to_string().to_uppercase()
}

pub(crate) fn action_output_ref(output_name: &str, output_uuid: &str) -> Value {
    json!({
        "Value": {
            "OutputName": output_name,
            "OutputUUID": output_uuid,
            "Type": "ActionOutput"
        },
        "WFSerializationType": "WFTextTokenAttachment"
    })
}

pub(crate) fn extension_input_text_token() -> Value {
    json!({
        "Value": {
            "attachmentsByRange": {
                "{0, 1}": {
                    "Type": "ExtensionInput"
                }
            },
            "string": "\u{fffc}"
        },
        "WFSerializationType": "WFTextTokenString"
    })
}

fn extension_input_attachment() -> Value {
    json!({
        "Value": {
            "Type": "ExtensionInput"
        },
        "WFSerializationType": "WFTextTokenAttachment"
    })
}

pub(crate) fn text_token_from_output(output_name: &str, output_uuid: &str) -> Value {
    json!({
        "Value": {
            "attachmentsByRange": {
                "{0, 1}": {
                    "OutputName": output_name,
                    "OutputUUID": output_uuid,
                    "Type": "ActionOutput"
                }
            },
            "string": "\u{fffc}"
        },
        "WFSerializationType": "WFTextTokenString"
    })
}

fn variable_ref(variable_name: &str) -> Value {
    json!({
        "Value": {
            "VariableName": variable_name,
            "Type": "Variable"
        },
        "WFSerializationType": "WFTextTokenAttachment"
    })
}

fn text_token_from_variable(variable_name: &str) -> Value {
    json!({
        "Value": {
            "attachmentsByRange": {
                "{0, 1}": {
                    "VariableName": variable_name,
                    "Type": "Variable"
                }
            },
            "string": "\u{fffc}"
        },
        "WFSerializationType": "WFTextTokenString"
    })
}

pub(crate) fn resolve_reference(
    outputs: &Map<String, Value>,
    reference: &ShortcutReference,
    as_text_token: bool,
) -> Result<Value, MacosError> {
    match reference {
        ShortcutReference::Output(name) => {
            let output = outputs
                .get(name)
                .ok_or_else(|| MacosError::Other(format!("unknown shortcut output reference: {name}")))?;
            let output_name = output
                .get("OutputName")
                .and_then(Value::as_str)
                .ok_or_else(|| MacosError::Other(format!("missing output name for reference: {name}")))?;
            let output_uuid = output
                .get("OutputUUID")
                .and_then(Value::as_str)
                .ok_or_else(|| MacosError::Other(format!("missing output uuid for reference: {name}")))?;
            Ok(if as_text_token {
                text_token_from_output(output_name, output_uuid)
            } else {
                action_output_ref(output_name, output_uuid)
            })
        }
        ShortcutReference::ExtensionInput if as_text_token => Ok(extension_input_text_token()),
        ShortcutReference::ExtensionInput => Ok(extension_input_attachment()),
        ShortcutReference::RepeatItem if as_text_token => Ok(text_token_from_variable("Repeat Item")),
        ShortcutReference::RepeatItem => Ok(variable_ref("Repeat Item")),
        ShortcutReference::RepeatIndex if as_text_token => Ok(text_token_from_variable("Repeat Index")),
        ShortcutReference::RepeatIndex => Ok(variable_ref("Repeat Index")),
    }
}

pub(crate) fn variable_wrapper(reference: Value) -> Value {
    json!({
        "Type": "Variable",
        "Variable": reference
    })
}

fn build_text_action(
    value: &str,
    output: Option<&str>,
    outputs: &mut Map<String, Value>,
) -> Value {
    let uuid = new_uuid();
    if let Some(output) = output {
        outputs.insert(
            output.to_string(),
            json!({
                "OutputName": output,
                "OutputUUID": uuid,
            }),
        );
    }
    let mut params = Map::new();
    params.insert("UUID".into(), json!(uuid));
    params.insert("WFTextActionText".into(), json!(value));
    if let Some(output) = output {
        params.insert("CustomOutputName".into(), json!(output));
    }
    json!({
        "WFWorkflowActionIdentifier": "is.workflow.actions.gettext",
        "WFWorkflowActionParameters": params
    })
}

fn build_get_text_action(
    from: &ShortcutReference,
    output: Option<&str>,
    outputs: &mut Map<String, Value>,
) -> Result<Value, MacosError> {
    let uuid = new_uuid();
    let resolved_input = resolve_reference(outputs, from, true)?;
    if let Some(output) = output {
        outputs.insert(
            output.to_string(),
            json!({
                "OutputName": output,
                "OutputUUID": uuid,
            }),
        );
    }
    let mut params = Map::new();
    params.insert("UUID".into(), json!(uuid));
    params.insert("WFTextActionText".into(), resolved_input);
    if let Some(output) = output {
        params.insert("CustomOutputName".into(), json!(output));
    }
    Ok(json!({
        "WFWorkflowActionIdentifier": "is.workflow.actions.gettext",
        "WFWorkflowActionParameters": params
    }))
}

fn build_get_urls_action(
    from: &ShortcutReference,
    output: Option<&str>,
    outputs: &mut Map<String, Value>,
) -> Result<Value, MacosError> {
    let uuid = new_uuid();
    let resolved_input = resolve_reference(outputs, from, false)?;
    if let Some(output) = output {
        outputs.insert(
            output.to_string(),
            json!({
                "OutputName": output,
                "OutputUUID": uuid,
            }),
        );
    }
    let mut params = Map::new();
    params.insert("UUID".into(), json!(uuid));
    params.insert("WFInput".into(), resolved_input);
    if let Some(output) = output {
        params.insert("CustomOutputName".into(), json!(output));
    }
    Ok(json!({
        "WFWorkflowActionIdentifier": "is.workflow.actions.detect.link",
        "WFWorkflowActionParameters": params
    }))
}

fn build_replace_text_action(
    from: &ShortcutReference,
    find: &str,
    replace: &str,
    regex: bool,
    ignore_case: bool,
    output: Option<&str>,
    outputs: &mut Map<String, Value>,
) -> Result<Value, MacosError> {
    let uuid = new_uuid();
    let resolved_input = resolve_reference(outputs, from, true)?;
    if let Some(output) = output {
        outputs.insert(
            output.to_string(),
            json!({
                "OutputName": output,
                "OutputUUID": uuid,
            }),
        );
    }
    let mut params = Map::new();
    params.insert("UUID".into(), json!(uuid));
    params.insert("WFInput".into(), resolved_input);
    params.insert("WFReplaceTextFind".into(), json!(find));
    params.insert("WFReplaceTextReplace".into(), json!(replace));
    params.insert("WFReplaceTextRegularExpression".into(), json!(regex));
    params.insert("WFReplaceTextCaseSensitive".into(), json!(!ignore_case));
    if let Some(output) = output {
        params.insert("CustomOutputName".into(), json!(output));
    }
    Ok(json!({
        "WFWorkflowActionIdentifier": "is.workflow.actions.text.replace",
        "WFWorkflowActionParameters": params
    }))
}

fn build_setclipboard_action(from: &ShortcutReference, outputs: &Map<String, Value>) -> Result<Value, MacosError> {
    Ok(json!({
        "WFWorkflowActionIdentifier": "is.workflow.actions.setclipboard",
        "WFWorkflowActionParameters": {
            "WFInput": resolve_reference(outputs, from, false)?,
        }
    }))
}

fn build_share_action(from: &ShortcutReference, outputs: &Map<String, Value>) -> Result<Value, MacosError> {
    Ok(json!({
        "WFWorkflowActionIdentifier": "is.workflow.actions.share",
        "WFWorkflowActionParameters": {
            "WFInput": resolve_reference(outputs, from, false)?,
        }
    }))
}

pub fn build_action(
    action: &ShortcutAction,
    outputs: &mut Map<String, Value>,
) -> Result<Value, MacosError> {
    match action {
        ShortcutAction::Text { value, output } => Ok(build_text_action(value, output.as_deref(), outputs)),
        ShortcutAction::GetText { from, output } => build_get_text_action(from, output.as_deref(), outputs),
        ShortcutAction::GetUrls { from, output } => build_get_urls_action(from, output.as_deref(), outputs),
        ShortcutAction::ReplaceText {
            from,
            find,
            replace,
            regex,
            ignore_case,
            output,
        } => build_replace_text_action(
            from,
            find,
            replace,
            *regex,
            *ignore_case,
            output.as_deref(),
            outputs,
        ),
        ShortcutAction::CopyToClipboard { from } => build_setclipboard_action(from, outputs),
        ShortcutAction::Share { from } => build_share_action(from, outputs),
        ShortcutAction::IfEqualsText { .. }
        | ShortcutAction::RepeatEach { .. } => Err(MacosError::Other(
            "shortcut action not yet supported by compiler".into(),
        )),
    }
}