npcrs 0.1.4

Rust core for the NPC system — agent kernel, jinx executor, LLM client
Documentation
use crate::error::Result;
use std::collections::HashMap;

pub fn action_space() -> HashMap<&'static str, &'static str> {
    let mut m = HashMap::new();
    m.insert("click", r#"{"x": "int (0-100)", "y": "int (0-100)"}"#);
    m.insert("type", r#"{"text": "string"}"#);
    m.insert("key", r#"{"keys": "list of key names"}"#);
    m.insert("shell", r#"{"command": "string"}"#);
    m.insert("wait", r#"{"duration": "float seconds"}"#);
    m.insert("hotkey", r#"{"keys": "list of keys"}"#);
    m.insert("scroll", r#"{"direction": "up|down", "amount": "int"}"#);
    m.insert("quit", r#"{"description": "goal complete"}"#);
    m
}

pub fn perform_action(action: &serde_json::Value) -> Result<HashMap<String, String>> {
    let action_type = action.get("type").and_then(|v| v.as_str()).unwrap_or("");
    let mut result = HashMap::new();
    match action_type {
        "click" => {
            let x = action
                .get("x")
                .and_then(|v| v.as_f64())
                .unwrap_or(50.0)
                .max(0.0)
                .min(100.0);
            let y = action
                .get("y")
                .and_then(|v| v.as_f64())
                .unwrap_or(50.0)
                .max(0.0)
                .min(100.0);
            let (w, h) = std::process::Command::new("xdotool")
                .arg("getdisplaygeometry")
                .output()
                .ok()
                .filter(|o| o.status.success())
                .map(|o| {
                    let s = String::from_utf8_lossy(&o.stdout);
                    let p: Vec<&str> = s.trim().split_whitespace().collect();
                    (
                        p.first()
                            .and_then(|p| p.parse::<f64>().ok())
                            .unwrap_or(1920.0),
                        p.get(1)
                            .and_then(|p| p.parse::<f64>().ok())
                            .unwrap_or(1080.0),
                    )
                })
                .unwrap_or((1920.0, 1080.0));
            let _ = std::process::Command::new("xdotool")
                .args([
                    "mousemove",
                    &((x * w / 100.0) as i64).to_string(),
                    &((y * h / 100.0) as i64).to_string(),
                    "click",
                    "1",
                ])
                .output();
            result.insert("status".into(), "success".into());
            result.insert("output".into(), format!("Clicked at ({}, {}).", x, y));
        }
        "type" => {
            let text = action.get("text").and_then(|v| v.as_str()).unwrap_or("");
            let _ = std::process::Command::new("xdotool")
                .args(["type", "--clearmodifiers", "--delay", "12", "--", text])
                .output();
            result.insert("status".into(), "success".into());
            result.insert("output".into(), format!("Typed '{}'.", text));
        }
        "key" => {
            let keys: Vec<String> = action
                .get("keys")
                .and_then(|v| v.as_array())
                .map(|a| {
                    a.iter()
                        .filter_map(|v| v.as_str().map(String::from))
                        .collect()
                })
                .or_else(|| {
                    action
                        .get("keys")
                        .and_then(|v| v.as_str())
                        .map(|s| vec![s.into()])
                })
                .unwrap_or_default();
            for k in &keys {
                let _ = std::process::Command::new("xdotool")
                    .args(["key", k])
                    .output();
            }
            result.insert("status".into(), "success".into());
            result.insert("output".into(), "Pressed key(s).".into());
        }
        "hotkey" => {
            let keys = action
                .get("keys")
                .and_then(|v| v.as_array())
                .map(|a| {
                    a.iter()
                        .filter_map(|v| v.as_str())
                        .collect::<Vec<_>>()
                        .join("+")
                })
                .unwrap_or_default();
            if !keys.is_empty() {
                let _ = std::process::Command::new("xdotool")
                    .args(["key", &keys])
                    .output();
            }
            result.insert("status".into(), "success".into());
            result.insert("output".into(), "Pressed hotkey.".into());
        }
        "shell" | "bash" => {
            let cmd = action.get("command").and_then(|v| v.as_str()).unwrap_or("");
            let _ = std::process::Command::new("sh").args(["-c", cmd]).spawn();
            result.insert("status".into(), "success".into());
            result.insert("output".into(), format!("Launched '{}'.", cmd));
        }
        "wait" => {
            let dur = action
                .get("duration")
                .and_then(|v| v.as_f64())
                .unwrap_or(1.0);
            std::thread::sleep(std::time::Duration::from_secs_f64(dur));
            result.insert("status".into(), "success".into());
            result.insert("output".into(), format!("Waited {}s.", dur));
        }
        "scroll" => {
            let dir = action
                .get("direction")
                .and_then(|v| v.as_str())
                .unwrap_or("down");
            let amt = action.get("amount").and_then(|v| v.as_i64()).unwrap_or(3);
            let btn = if dir == "up" { "4" } else { "5" };
            for _ in 0..amt.unsigned_abs() {
                let _ = std::process::Command::new("xdotool")
                    .args(["click", btn])
                    .output();
            }
            result.insert("status".into(), "success".into());
            result.insert("output".into(), format!("Scrolled {} by {}.", dir, amt));
        }
        other => {
            result.insert("status".into(), "error".into());
            result.insert("message".into(), format!("Unknown action: {}", other));
        }
    }
    Ok(result)
}