use anyhow::Result;
use serde::Serialize;
use crate::session::SUPPORTED_SCHEMA;
pub const MIN_PIXELCOORDS: &str = "0.1.2";
fn parts(version: &str) -> Option<(u32, u32, u32)> {
let mut fields = version.trim().split('.');
let major = fields.next()?.parse().ok()?;
let minor = fields.next()?.parse().ok()?;
let patch = fields.next()?.split(['-', '+']).next()?.parse().ok()?;
if fields.next().is_some() {
return None;
}
Some((major, minor, patch))
}
pub fn meets_minimum(found: &str) -> bool {
let (Some(found), Some(needed)) = (parts(found), parts(MIN_PIXELCOORDS)) else {
return false;
};
found >= needed
}
pub fn require_supported_pixelcoords() -> Result<(), String> {
let status = pixelcoords_status();
if !status.found {
return Err(
"pixelcoords is not on PATH — it is what relocates and verifies regions. \
Install it with `cargo install pixelcoords`"
.to_string(),
);
}
let Some(version) = status.version else {
return Err(
"could not read `pixelcoords --version`, so its version cannot be trusted. \
Reinstall with `cargo install pixelcoords`"
.to_string(),
);
};
if !meets_minimum(&version) {
return Err(format!(
"pixelcoords {version} is too old — this build needs {MIN_PIXELCOORDS} or newer. \
Older captures composite the mouse pointer into the image, which makes \
relocation unreliable in a way that looks like flakiness. \
Upgrade with `cargo install pixelcoords`"
));
}
Ok(())
}
#[derive(Debug, Serialize)]
struct Report {
schema: u32,
platform: &'static str,
supported_platform: bool,
native_space: pixelactions_core::convert::Space,
session_schema_supported: u32,
pixelcoords: PixelcoordsStatus,
capabilities: Capabilities,
accessibility_trusted: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
linux: Option<LinuxStatus>,
probe: Probe,
}
#[derive(Debug, Serialize)]
struct LinuxStatus {
server: pixelactions_core::display::Server,
rung: &'static str,
portal_remote_desktop_version: u32,
portal_screen_cast_version: u32,
portal_device_types: u32,
cursor_metadata_available: bool,
grant_remembered: bool,
}
#[derive(Debug, Serialize)]
struct PixelcoordsStatus {
found: bool,
version: Option<String>,
minimum: &'static str,
}
#[derive(Debug, Serialize)]
struct Capabilities {
resolve: bool,
inject: bool,
verify: bool,
}
#[derive(Debug, Serialize)]
struct Probe {
attempted: bool,
moved: bool,
confirmed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
detail: Option<String>,
}
fn pixelcoords_status() -> PixelcoordsStatus {
let output = std::process::Command::new("pixelcoords")
.arg("--version")
.output();
let Ok(output) = output else {
return PixelcoordsStatus {
found: false,
version: None,
minimum: MIN_PIXELCOORDS,
};
};
let text = String::from_utf8_lossy(&output.stdout);
let version = text.split_whitespace().nth(1).map(str::to_string);
PixelcoordsStatus {
found: true,
version,
minimum: MIN_PIXELCOORDS,
}
}
pub fn run(json: bool, probe: bool) -> Result<i32> {
let probe_result = run_probe(probe);
let can_inject = crate::inject::availability();
let report = Report {
schema: 1,
platform: std::env::consts::OS,
supported_platform: can_inject.is_ok(),
native_space: pixelactions_core::convert::native_space(),
session_schema_supported: SUPPORTED_SCHEMA,
pixelcoords: pixelcoords_status(),
capabilities: Capabilities {
resolve: true,
inject: can_inject.is_ok(),
verify: true,
},
accessibility_trusted: trusted(),
linux: linux_status(),
probe: probe_result,
};
let refusal = can_inject.err();
if json {
println!("{}", serde_json::to_string_pretty(&report)?);
return Ok(0);
}
println!("platform: {}", report.platform);
println!(
"supported: {}",
if report.supported_platform {
"yes".to_string()
} else {
format!("no — {}", refusal.as_deref().unwrap_or("unsupported"))
}
);
if let Some(linux) = &report.linux {
print_linux(linux);
}
println!("native space: {:?}", report.native_space);
println!(
"session schema: {} and older",
report.session_schema_supported
);
match (&report.pixelcoords.found, &report.pixelcoords.version) {
(true, Some(version)) => {
let verdict = if meets_minimum(version) {
"ok"
} else {
"TOO OLD"
};
println!("pixelcoords: {version} (minimum {MIN_PIXELCOORDS}) — {verdict}");
}
(true, None) => println!("pixelcoords: found, version unreadable"),
(false, _) => println!("pixelcoords: not on PATH — needed to relocate and verify"),
}
println!();
println!("capabilities:");
println!(" resolve a plan yes");
println!(" inject input {}", inject_line(&report));
println!(" verify a step yes — via pixelcoords find");
if report.probe.attempted {
println!();
match (
report.probe.moved,
report.probe.confirmed,
&report.probe.detail,
) {
(true, true, _) => {
println!("probe: the cursor moved — input permission is real");
}
(true, false, detail) => {
println!("probe: input was granted and accepted, NOT confirmed");
if let Some(detail) = detail {
println!(" {detail}");
}
}
(false, _, Some(detail)) => println!("probe: FAILED\n {detail}"),
(false, _, None) => println!("probe: failed, no detail"),
}
}
if report.probe.attempted && !report.probe.moved {
return Ok(3);
}
Ok(0)
}
fn inject_line(report: &Report) -> String {
if !report.capabilities.inject {
return "no".to_string();
}
let Some(linux) = &report.linux else {
return "yes — needs macOS Accessibility permission".to_string();
};
let grant = if linux.grant_remembered {
"a remembered screen-share grant"
} else {
"a screen-share grant you approve once"
};
format!("yes — via {} using {grant}", linux.rung)
}
fn print_linux(linux: &LinuxStatus) {
println!("session: {}", linux.server.name());
println!("input path: {}", linux.rung);
println!(
"portal: RemoteDesktop v{} · ScreenCast v{} · devices {:#b}",
linux.portal_remote_desktop_version,
linux.portal_screen_cast_version,
linux.portal_device_types
);
println!(
"grant: {}",
if linux.grant_remembered {
"remembered — no dialog expected"
} else {
"not yet given — the first run will ask"
}
);
println!(
"kill switch: {}",
if linux.cursor_metadata_available {
"no eyes on Wayland in this build (the compositor could provide them)"
} else {
"no eyes on Wayland, and this compositor offers no cursor metadata"
}
);
}
#[cfg(target_os = "linux")]
fn linux_status() -> Option<LinuxStatus> {
use pixelactions_core::display::{Server, detect};
let server = detect(
std::env::var("XDG_SESSION_TYPE").ok().as_deref(),
std::env::var("WAYLAND_DISPLAY").ok().as_deref(),
std::env::var("DISPLAY").ok().as_deref(),
);
let portal = match server {
Server::Wayland => crate::portal::capabilities().ok(),
_ => None,
};
let Some(portal) = portal else {
return Some(LinuxStatus {
server,
rung: "none",
portal_remote_desktop_version: 0,
portal_screen_cast_version: 0,
portal_device_types: 0,
cursor_metadata_available: false,
grant_remembered: false,
});
};
Some(LinuxStatus {
server,
rung: if portal.usable() {
"portal RemoteDesktop + EIS"
} else {
"none"
},
portal_remote_desktop_version: portal.remote_desktop_version,
portal_screen_cast_version: portal.screen_cast_version,
portal_device_types: portal.device_types,
cursor_metadata_available: portal.cursor_metadata(),
grant_remembered: portal.have_stored_token,
})
}
#[cfg(not(target_os = "linux"))]
fn linux_status() -> Option<LinuxStatus> {
None
}
fn trusted() -> Option<bool> {
#[cfg(target_os = "macos")]
{
Some(crate::mac::is_trusted())
}
#[cfg(not(target_os = "macos"))]
{
None
}
}
#[cfg(target_os = "macos")]
fn run_probe(requested: bool) -> Probe {
if !requested {
return Probe {
attempted: false,
moved: false,
confirmed: false,
detail: None,
};
}
if !crate::mac::is_trusted() {
crate::mac::request_trust();
return Probe {
attempted: true,
moved: false,
confirmed: false,
detail: Some(
"Accessibility is not granted to the application running pixelactions, \
so synthetic events would be discarded silently. A system dialog was \
just requested — approve it, or add the app under System Settings > \
Privacy & Security > Accessibility, then quit and reopen it and run \
this again. The grant attaches to the app you launched from (your \
terminal), not to the pixelactions binary."
.to_string(),
),
};
}
let outcome = crate::inject::RealInjector::new().and_then(|mut injector| {
use crate::inject::Injector;
injector.probe()
});
match outcome {
Ok(()) => Probe {
attempted: true,
moved: true,
confirmed: true,
detail: None,
},
Err(error) => Probe {
attempted: true,
moved: false,
confirmed: false,
detail: Some(format!("{error:#}")),
},
}
}
#[cfg(target_os = "linux")]
fn run_probe(requested: bool) -> Probe {
if !requested {
return Probe {
attempted: false,
moved: false,
confirmed: false,
detail: None,
};
}
if let Err(reason) = crate::inject::availability() {
return Probe {
attempted: true,
moved: false,
confirmed: false,
detail: Some(reason),
};
}
let outcome = crate::inject::RealInjector::new(&[]).and_then(|mut injector| {
use crate::inject::Injector;
injector.probe().map(|()| {
let regions = injector.regions().len();
let typing = injector.can_type();
(regions, typing)
})
});
match outcome {
Ok((regions, typing)) => Probe {
attempted: true,
moved: true,
confirmed: false,
detail: Some(format!(
"the compositor granted input and described {regions} region(s); typing is \
{}. Whether the pointer moved cannot be checked — Wayland exposes no way \
to ask where it is, which is also why the corner kill switch has nothing \
to watch here",
if typing {
"available"
} else {
"unavailable (no keymap was sent)"
}
)),
},
Err(error) => Probe {
attempted: true,
moved: false,
confirmed: false,
detail: Some(format!("{error:#}")),
},
}
}
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
fn run_probe(requested: bool) -> Probe {
Probe {
attempted: requested,
moved: false,
confirmed: false,
detail: requested
.then(|| "input synthesis is not implemented for this platform yet".to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_declared_minimum_is_itself_readable() {
assert!(parts(MIN_PIXELCOORDS).is_some(), "{MIN_PIXELCOORDS}");
}
#[test]
fn newer_and_equal_versions_are_accepted() {
assert!(meets_minimum(MIN_PIXELCOORDS));
assert!(meets_minimum("0.1.3"));
assert!(meets_minimum("0.2.0"));
assert!(meets_minimum("1.0.0"));
assert!(meets_minimum("0.1.2-rc1"));
}
#[test]
fn older_versions_are_refused() {
assert!(!meets_minimum("0.1.1"));
assert!(!meets_minimum("0.1.0"));
assert!(!meets_minimum("0.0.9"));
}
#[test]
fn an_unreadable_version_is_refused_rather_than_assumed_good() {
for bad in ["", "0.1", "0.1.2.3", "banana", "v0.1.2", "0.x.2"] {
assert!(!meets_minimum(bad), "should refuse {bad:?}");
}
}
}