use crate::error::TalkError;
use crate::paste::node::{PasteCtx, PasteNode};
use async_trait::async_trait;
pub(crate) struct XtestTypeNode {}
const KEY_SHIFT_L: u32 = 0xffe1;
fn keysyms_for_char(c: char) -> Option<Vec<u32>> {
let cp = c as u32;
if cp > 0xFF {
return None;
}
if c.is_ascii_uppercase() {
Some(vec![KEY_SHIFT_L, cp])
} else {
Some(vec![cp])
}
}
#[async_trait]
impl PasteNode for XtestTypeNode {
async fn paste(&self, text: &str, _ctx: &PasteCtx<'_>) -> Result<(), TalkError> {
for c in text.chars() {
let syms = match keysyms_for_char(c) {
Some(s) => s,
None => {
log::warn!(
"paste(xtest-type): character U+{:04X} out of Latin-1 range — skipped \
(Phase 1 XTest typing supports ASCII/Latin-1 only)",
c as u32,
);
continue;
}
};
let ok = tokio::task::spawn_blocking(move || crate::x11::x11_send_key_combo(&syms))
.await
.unwrap_or(false);
if !ok {
return Err(TalkError::Clipboard(format!(
"XTest typing failed for character {:?}",
c,
)));
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::keysyms_for_char;
#[test]
fn ascii_lowercase_no_shift() {
assert_eq!(keysyms_for_char('a'), Some(vec![0x61]));
}
#[test]
fn ascii_uppercase_uses_shift() {
assert_eq!(keysyms_for_char('A'), Some(vec![0xffe1, 0x41]));
}
#[test]
fn space_is_supported() {
assert_eq!(keysyms_for_char(' '), Some(vec![0x20]));
}
#[test]
fn non_latin1_returns_none() {
assert!(keysyms_for_char('é').is_some());
assert_eq!(keysyms_for_char('😀'), None);
assert_eq!(keysyms_for_char('漢'), None);
}
}