use actl_core::target::Target;
use actl_core::{CtlError, ErrorCode};
use uiautomation::UIElement;
use windows::Win32::Foundation::HGLOBAL;
use windows::Win32::System::DataExchange::{
CloseClipboard, EmptyClipboard, GetClipboardData, OpenClipboard, SetClipboardData,
};
use windows::Win32::System::Memory::{GMEM_MOVEABLE, GlobalAlloc, GlobalLock, GlobalUnlock};
use crate::locate::{Located, locate};
pub const PROPS: &[&str] = &[
"name",
"role",
"automation_id",
"offscreen",
"value",
"text",
];
pub struct PropertyInfo {
pub name: Option<String>,
pub role: String,
pub automation_id: Option<String>,
pub offscreen: Option<bool>,
pub value: Option<String>,
pub value_via: Option<&'static str>,
pub text: Option<String>,
pub unsupported: Vec<&'static str>,
}
pub fn read_property(
app: Option<&str>,
target: &Target,
property: Option<&str>,
near: Option<&str>,
) -> Result<PropertyInfo, CtlError> {
if let Some(p) = property
&& !PROPS.contains(&p)
{
return Err(CtlError::protocol(format!(
"unknown property {p:?}: expected one of {PROPS:?}"
)));
}
let loc = locate(app, target, near)?;
let want = |p: &str| property.map(|sel| sel == p).unwrap_or(true);
let (value, value_via) = if want("value") {
read_value_via(&loc.element)?
} else {
(None, None)
};
let text = if want("text") {
read_text(&loc.element)?
} else {
None
};
let mut unsupported = Vec::new();
if want("value") && value.is_none() {
unsupported.push("value");
}
if want("text") && text.is_none() {
unsupported.push("text");
}
if property == Some("value") {
crate::read_channel::require(value.as_ref(), "value-or-text-pattern")?;
}
if property == Some("text") {
crate::read_channel::require(text.as_ref(), "text-pattern")?;
}
Ok(PropertyInfo {
name: if want("name") {
Some(
loc.element
.get_name()
.map_err(|e| crate::read_channel::failure("name", e))?,
)
} else {
None
},
role: if want("role") {
format!(
"{:?}",
loc.element
.get_control_type()
.map_err(|e| crate::read_channel::failure("role", e))?
)
} else {
String::new()
},
automation_id: if want("automation_id") {
Some(
loc.element
.get_automation_id()
.map_err(|e| crate::read_channel::failure("automation_id", e))?,
)
} else {
None
},
offscreen: if want("offscreen") {
Some(
loc.element
.is_offscreen()
.map_err(|e| crate::read_channel::failure("offscreen", e))?,
)
} else {
None
},
value,
value_via,
text,
unsupported,
})
}
fn read_value_via(elem: &UIElement) -> Result<(Option<String>, Option<&'static str>), CtlError> {
crate::read_channel::choose(crate::read_channel::value(elem), || read_text(elem))
}
pub(crate) fn read_value(elem: &UIElement) -> Result<Option<String>, CtlError> {
Ok(read_value_via(elem)?.0)
}
pub(crate) fn read_text(elem: &UIElement) -> Result<Option<String>, CtlError> {
crate::read_channel::text(elem)
}
pub enum VerifyKind {
Exists,
Value {
expected: String,
contains: bool,
property: String,
},
Checked,
}
pub struct VerifyOutcome {
pub located: Located,
pub actual: Option<String>,
pub mode: &'static str,
pub channel: Option<&'static str>,
}
pub fn verify(
app: Option<&str>,
target: &Target,
near: Option<&str>,
kind: VerifyKind,
) -> Result<VerifyOutcome, CtlError> {
let fail = |what: &str, actual: Option<String>| {
CtlError::with_evidence(
ErrorCode::AssertionFailed,
format!("verify {what} failed"),
serde_json::json!({ "actual": actual }),
)
};
match kind {
VerifyKind::Exists => {
let loc = locate(app, target, near)?;
Ok(VerifyOutcome {
located: loc,
actual: None,
mode: "exists",
channel: None,
})
}
VerifyKind::Value {
expected,
contains,
property,
} => {
let loc = locate(app, target, near)?;
let (actual, channel) = match property.as_str() {
"name" => (
Some(
loc.element
.get_name()
.map_err(|e| crate::read_channel::failure("name", e))?,
),
Some("name"),
),
"text" => (read_text(&loc.element)?, Some("text-pattern")),
"value" => (
crate::read_channel::value(&loc.element)?,
Some("value-pattern"),
),
_ => read_value_via(&loc.element)?,
};
crate::read_channel::require(
actual.as_ref(),
channel.unwrap_or("value-or-text-pattern"),
)?;
let ok = match (&actual, contains) {
(Some(a), true) => a.contains(&expected),
(Some(a), false) => a == &expected,
(None, _) => false,
};
if !ok {
return Err(CtlError::with_evidence(
ErrorCode::AssertionFailed,
"verify result did not match",
serde_json::json!({
"target":target.describe(),"property":if property == "auto" {"value"} else {&property},"channel":channel,
"expected":expected,"actual":actual,"comparison":if contains {"contains"} else {"exact"}
}),
));
}
Ok(VerifyOutcome {
located: loc,
actual,
mode: match (property.as_str(), contains) {
("name", false) => "name",
("name", true) => "name-contains",
("text", false) => "text",
("text", true) => "text-contains",
(_, true) => "value-contains",
_ => "value",
},
channel,
})
}
VerifyKind::Checked => {
let loc = locate(app, target, near)?;
let state = crate::read_channel::toggle(&loc.element)?;
crate::read_channel::require(state.as_ref(), "toggle-pattern")?;
if state.as_deref() != Some("On") {
return Err(fail("checked", state));
}
Ok(VerifyOutcome {
located: loc,
actual: state,
mode: "checked",
channel: Some("toggle-pattern"),
})
}
}
}
const CF_UNICODETEXT: u32 = 13;
struct ClipGuard;
impl Drop for ClipGuard {
fn drop(&mut self) {
unsafe { CloseClipboard() }.ok();
}
}
fn open_clipboard() -> Result<ClipGuard, CtlError> {
if unsafe { OpenClipboard(None) }.is_ok() {
Ok(ClipGuard)
} else {
Err(CtlError::new(
ErrorCode::NotActionable,
"clipboard is held by another process; retry shortly",
))
}
}
pub fn get_clipboard_text() -> Result<Option<String>, CtlError> {
let mut action = actl_core::signal_session::ActionGuard::begin(
"get-clipboard",
actl_core::activity::InputEffects {
clipboard: actl_core::activity::ClipboardEffect::Read,
..Default::default()
},
Default::default(),
);
action.executing();
let result = get_clipboard_text_inner()?;
action.delivered();
Ok(result)
}
fn get_clipboard_text_inner() -> Result<Option<String>, CtlError> {
unsafe {
let _guard = open_clipboard()?;
if !windows::Win32::System::DataExchange::IsClipboardFormatAvailable(CF_UNICODETEXT).is_ok()
{
return Ok(None);
}
let Ok(h) = GetClipboardData(CF_UNICODETEXT) else {
return Ok(None);
};
let hg = HGLOBAL(h.0);
let p = GlobalLock(hg) as *const u16;
if p.is_null() {
return Ok(None);
}
let mut len = 0usize;
while *p.add(len) != 0 {
len += 1;
}
let text = String::from_utf16_lossy(std::slice::from_raw_parts(p, len));
let _ = GlobalUnlock(hg);
Ok(Some(text))
}
}
pub fn set_clipboard_text(text: &str) -> Result<(), CtlError> {
use windows::Win32::Foundation::{GlobalFree, HANDLE};
let mut action = crate::feedback::prepare(
"set-clipboard",
actl_core::activity::InputEffects {
clipboard: actl_core::activity::ClipboardEffect::Write,
..Default::default()
},
Default::default(),
)?;
crate::feedback::check_stop()?;
action.executing();
let mut wide: Vec<u16> = text.encode_utf16().collect();
wide.push(0);
unsafe {
let _guard = open_clipboard()?;
EmptyClipboard().map_err(|e| CtlError::internal(format!("EmptyClipboard: {e}")))?;
let h = GlobalAlloc(GMEM_MOVEABLE, wide.len() * 2)
.map_err(|e| CtlError::internal(format!("GlobalAlloc: {e}")))?;
let p = GlobalLock(h) as *mut u16;
if p.is_null() {
let _ = GlobalFree(Some(h));
return Err(CtlError::internal("GlobalLock failed"));
}
std::ptr::copy_nonoverlapping(wide.as_ptr(), p, wide.len());
let _ = GlobalUnlock(h);
if SetClipboardData(CF_UNICODETEXT, Some(HANDLE(h.0))).is_err() {
let _ = GlobalFree(Some(h));
return Err(CtlError::internal("SetClipboardData failed"));
}
action.delivered();
Ok(())
}
}