use xa11y::Key;
pub const MAX_WAIT_MS: u64 = 10_000;
#[derive(Debug, Clone, Default)]
pub struct KeyboardStep<'a> {
pub key: Option<&'a str>,
pub text: Option<&'a str>,
pub held: Option<&'a [String]>,
}
pub fn is_keyboard_step(key: Option<&str>, text: Option<&str>) -> bool {
key.is_some_and(|k| !k.trim().is_empty()) || text.is_some_and(|t| !t.is_empty())
}
pub fn is_wait_step(wait_ms: Option<u64>) -> bool {
wait_ms.is_some()
}
pub fn run_keyboard_step(
sim: &xa11y::InputSim,
step: &KeyboardStep,
tool: &str,
) -> Result<String, String> {
if let Some(text) = step.text.filter(|t| !t.is_empty()) {
sim.keyboard()
.type_text(text)
.map_err(|e| super::errors::render(tool, "type text", &e))?;
return Ok(format!("typed {text:?}"));
}
let key = step.key.unwrap_or_default().trim();
let parsed = parse_key(key, tool)?;
let held = parse_keys(step.held.unwrap_or_default(), tool)?;
if held.is_empty() {
sim.keyboard()
.press(parsed)
.map_err(|e| super::errors::render(tool, &format!("press {key}"), &e))?;
Ok(format!("pressed {key}"))
} else {
let names: Vec<String> = held.iter().map(key_name).collect();
sim.keyboard()
.chord(parsed, &held)
.map_err(|e| super::errors::render(tool, &format!("chord {key} + {names:?}"), &e))?;
Ok(format!("pressed {key} with {} held", names.join(",")))
}
}
pub fn run_wait_step(wait_ms: u64) -> String {
std::thread::sleep(std::time::Duration::from_millis(wait_ms));
format!("waited {wait_ms} ms")
}
pub fn parse_key(name: &str, tool: &str) -> Result<Key, String> {
let lower = name.to_ascii_lowercase();
let key = match lower.as_str() {
"enter" | "return" => Key::Enter,
"escape" | "esc" => Key::Escape,
"backspace" => Key::Backspace,
"tab" => Key::Tab,
"space" => Key::Space,
"delete" | "del" => Key::Delete,
"insert" => Key::Insert,
"up" | "arrowup" => Key::ArrowUp,
"down" | "arrowdown" => Key::ArrowDown,
"left" | "arrowleft" => Key::ArrowLeft,
"right" | "arrowright" => Key::ArrowRight,
"home" => Key::Home,
"end" => Key::End,
"pageup" => Key::PageUp,
"pagedown" => Key::PageDown,
_ if name.chars().count() == 1 => {
let ch = name.chars().next().unwrap_or_default();
if ch.is_uppercase() {
return Err(format!(
"{tool}: uppercase `{name}` — pass the lowercase key with `held: [\"shift\"]`"
));
}
Key::Char(ch)
}
_ if lower.starts_with('f')
&& lower.len() <= 3
&& lower[1..]
.parse::<u8>()
.is_ok_and(|n| (1..=12).contains(&n)) =>
{
Key::F(lower[1..].parse::<u8>().unwrap_or(1))
}
_ => {
return Err(format!("{tool}: unknown key `{name}`"));
}
};
Ok(key)
}
pub fn parse_keys(names: &[String], tool: &str) -> Result<Vec<Key>, String> {
names
.iter()
.map(|n| match n.to_ascii_lowercase().as_str() {
"shift" => Ok(Key::Shift),
"ctrl" | "control" => Ok(Key::Ctrl),
"alt" | "option" => Ok(Key::Alt),
"meta" | "cmd" | "command" | "super" | "win" => Ok(Key::Meta),
other => Err(format!(
"{tool}: unknown modifier `{other}` (expected shift, ctrl, alt or meta)"
)),
})
.collect()
}
pub fn key_name(key: &Key) -> String {
match key {
Key::Shift => "shift".to_string(),
Key::Ctrl => "ctrl".to_string(),
Key::Alt => "alt".to_string(),
Key::Meta => "meta".to_string(),
Key::Char(c) => c.to_string(),
other => format!("{other:?}"),
}
}
pub(crate) fn shared_sim() -> Result<xa11y::InputSim, String> {
super::shared_input_sim()
}