use smix_input::{KeyName, SwipeDirection};
use smix_recorder_ir::{IRAction, RecorderError, RecorderErrorReason};
use smix_selector::{Modifiers, Pattern, Selector};
pub fn generate_rust(
actions: &[IRAction],
test_fn_name: &str,
bundle_id: &str,
) -> Result<String, RecorderError> {
if actions.is_empty() {
return Err(RecorderError::new(
RecorderErrorReason::EmptySession,
"cannot emit Rust source from zero actions",
));
}
let mut out = String::with_capacity(2048);
out.push_str("//! Generated by smix-recorder.\n//! Edit as needed; the AI cleanup pass\n//! may have already added wait_for hints between phases.\n\n");
out.push_str("use smix_sdk::{App, KeyName, SwipeDirection, focused, id, label, role, role_named, text, text_regex};\n");
out.push_str("use std::time::Duration;\n\n");
out.push_str("#[tokio::test(flavor = \"multi_thread\", worker_threads = 2)]\n");
out.push_str(&format!(
"async fn {test_fn_name}() -> Result<(), smix_sdk::ExpectationFailure> {{\n"
));
out.push_str(" let port: u16 = std::env::var(\"SMIX_RUNNER_PORT\")\n");
out.push_str(" .ok()\n");
out.push_str(" .and_then(|s| s.parse().ok())\n");
out.push_str(" .unwrap_or(22087);\n");
out.push_str(" let udid = std::env::var(\"SMIX_UDID\")\n");
out.push_str(" .expect(\"SMIX_UDID env var required\");\n");
out.push_str(" let app = App::connect_to_runner(port).await?.with_udid(udid);\n");
out.push_str(&format!(" app.launch(\"{bundle_id}\").await?;\n"));
out.push('\n');
for a in actions {
emit_action(&mut out, a);
}
out.push_str("\n Ok(())\n}\n");
Ok(out)
}
fn emit_action(out: &mut String, action: &IRAction) {
match action {
IRAction::Tap { selector, .. } => {
out.push_str(&format!(
" app.tap(&{}).await?;\n",
emit_selector(selector)
));
}
IRAction::Fill { selector, text, .. } => {
out.push_str(&format!(
" app.fill(&{}, {}).await?;\n",
emit_selector(selector),
emit_rust_str_literal(text)
));
}
IRAction::Clear { selector, .. } => {
out.push_str(&format!(
" app.clear(&{}).await?;\n",
emit_selector(selector)
));
}
IRAction::PressKey { key, .. } => {
out.push_str(&format!(" app.press_key({}).await?;\n", emit_key(*key)));
}
IRAction::Swipe {
direction, from, ..
} => match from {
Some(from_sel) => {
out.push_str(&format!(
" // captured swipe from: {} — App.swipe_once does not accept a from selector;\n // adjust the test manually or open an issue.\n app.swipe_once({}).await?;\n",
emit_selector(from_sel),
emit_swipe(*direction),
));
}
None => {
out.push_str(&format!(
" app.swipe_once({}).await?;\n",
emit_swipe(*direction)
));
}
},
IRAction::GoBack { .. } => {
out.push_str(" app.go_back().await?;\n");
}
IRAction::WaitFor { selector, .. } => {
out.push_str(&format!(
" app.wait_for(&{}, Duration::from_secs(5)).await?;\n",
emit_selector(selector)
));
}
IRAction::HideKeyboard { .. } => {
out.push_str(" app.hide_keyboard().await?;\n");
}
}
}
fn emit_selector(s: &Selector) -> String {
match s {
Selector::Text { text, modifiers } if is_empty_modifiers(modifiers) => match text {
Pattern::Text(lit) => format!("text({})", emit_rust_str_literal(lit)),
Pattern::Regex { regex, .. } => format!("text_regex({})", emit_rust_str_literal(regex)),
},
Selector::Id { id: x, modifiers } if is_empty_modifiers(modifiers) => {
format!("id({})", emit_rust_str_literal(x))
}
Selector::Label {
label: x,
modifiers,
} if is_empty_modifiers(modifiers) => {
format!("label({})", emit_rust_str_literal(x))
}
Selector::Role {
role: r,
name: None,
modifiers,
} if is_empty_modifiers(modifiers) => {
format!("role(smix_sdk::Role::{:?})", r)
}
Selector::Role {
role: r,
name: Some(Pattern::Text(n)),
modifiers,
} if is_empty_modifiers(modifiers) => {
format!(
"role_named(smix_sdk::Role::{:?}, {})",
r,
emit_rust_str_literal(n)
)
}
Selector::Focused { .. } => "focused()".into(),
_ => {
let json = serde_json::to_string(s).unwrap_or_else(|_| "null".into());
format!(
"serde_json::from_str::<smix_sdk::Selector>({}).expect(\"selector\")",
emit_rust_str_literal(&json)
)
}
}
}
fn emit_key(k: KeyName) -> &'static str {
match k {
KeyName::Return => "KeyName::Return",
KeyName::Delete => "KeyName::Delete",
KeyName::Tab => "KeyName::Tab",
KeyName::Space => "KeyName::Space",
KeyName::Escape => "KeyName::Escape",
KeyName::ArrowUp => "KeyName::ArrowUp",
KeyName::ArrowDown => "KeyName::ArrowDown",
KeyName::ArrowLeft => "KeyName::ArrowLeft",
KeyName::ArrowRight => "KeyName::ArrowRight",
KeyName::Home => "KeyName::Home",
KeyName::Lock => "KeyName::Lock",
KeyName::VolumeUp => "KeyName::VolumeUp",
KeyName::VolumeDown => "KeyName::VolumeDown",
}
}
fn emit_swipe(d: SwipeDirection) -> &'static str {
match d {
SwipeDirection::Up => "SwipeDirection::Up",
SwipeDirection::Down => "SwipeDirection::Down",
SwipeDirection::Left => "SwipeDirection::Left",
SwipeDirection::Right => "SwipeDirection::Right",
}
}
fn is_empty_modifiers(m: &Modifiers) -> bool {
m.near.is_none()
&& m.below.is_none()
&& m.above.is_none()
&& m.left_of.is_none()
&& m.right_of.is_none()
&& m.inside.is_none()
&& m.nth.is_none()
&& m.first.is_none()
&& m.last.is_none()
}
fn emit_rust_str_literal(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c => out.push(c),
}
}
out.push('"');
out
}