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,
#[serde(skip_serializing_if = "Option::is_none")]
display: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
connected: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
portal_remote_desktop_version: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
portal_screen_cast_version: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
portal_device_types: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
cursor_metadata_available: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
grant_remembered: Option<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, and the OS confirmed where it went");
}
(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();
};
if linux.rung == "none" {
return "no — this session has no input path".to_string();
}
format!("yes — via {}, {}", linux.rung, grant_cost(linux))
}
fn grant_cost(linux: &LinuxStatus) -> &'static str {
match (linux.server, linux.grant_remembered) {
(pixelactions_core::display::Server::X11, _) => "which asks nothing of you",
(_, Some(true)) => "using a remembered screen-share grant",
_ => "using a screen-share grant you approve once",
}
}
fn display_line(linux: &LinuxStatus) -> Option<String> {
let verdict = if linux.connected? {
"connected"
} else {
"no answer"
};
let display = linux.display.as_deref().unwrap_or("(unset)");
Some(format!("{display} — {verdict}"))
}
fn portal_line(linux: &LinuxStatus) -> Option<String> {
Some(format!(
"RemoteDesktop v{} · ScreenCast v{} · devices {:#b}",
linux.portal_remote_desktop_version?,
linux.portal_screen_cast_version.unwrap_or(0),
linux.portal_device_types.unwrap_or(0)
))
}
fn grant_line(linux: &LinuxStatus) -> &'static str {
use pixelactions_core::display::Server;
match (linux.server, linux.grant_remembered) {
(Server::X11, _) => {
"none needed — any X client may inject into any other, which is the hole Wayland closes"
}
(_, Some(true)) => "remembered — no dialog expected",
(_, Some(false)) => "not yet given — the first run will ask",
(_, None) => "nothing to grant — this session has no input path",
}
}
fn kill_switch_line(linux: &LinuxStatus) -> &'static str {
use pixelactions_core::display::Server;
match (linux.server, linux.cursor_metadata_available) {
(Server::X11, _) => "armed — X11 reports the pointer position, so the corner check works",
(Server::Wayland, Some(true)) => {
"no eyes on Wayland in this build (the compositor could provide them)"
}
(Server::Wayland, _) => "no eyes on Wayland, and this compositor offers no cursor metadata",
(Server::Unknown, _) => "nothing to watch — no session was found",
}
}
fn print_linux(linux: &LinuxStatus) {
println!("session: {}", linux.server.name());
println!("input path: {}", linux.rung);
if let Some(display) = display_line(linux) {
println!("display: {display}");
}
if let Some(portal) = portal_line(linux) {
println!("portal: {portal}");
}
println!("grant: {}", grant_line(linux));
println!("kill switch: {}", kill_switch_line(linux));
}
#[cfg(any(target_os = "linux", test))]
fn no_input_path(server: pixelactions_core::display::Server) -> LinuxStatus {
LinuxStatus {
server,
rung: "none",
display: None,
connected: None,
portal_remote_desktop_version: None,
portal_screen_cast_version: None,
portal_device_types: None,
cursor_metadata_available: None,
grant_remembered: None,
}
}
#[cfg(target_os = "linux")]
fn linux_status() -> Option<LinuxStatus> {
use pixelactions_core::display::Server;
let server = crate::inject::session_server();
let status = match server {
Server::X11 => x11_status(),
Server::Wayland => wayland_status(),
Server::Unknown => no_input_path(server),
};
Some(status)
}
#[cfg(target_os = "linux")]
fn x11_status() -> LinuxStatus {
use pixelactions_core::display::Server;
let connected = crate::inject::X11Injector::new().is_ok();
LinuxStatus {
rung: if connected {
"XTEST on the root window"
} else {
"none"
},
display: std::env::var("DISPLAY")
.ok()
.filter(|value| !value.trim().is_empty()),
connected: Some(connected),
..no_input_path(Server::X11)
}
}
#[cfg(target_os = "linux")]
fn wayland_status() -> LinuxStatus {
use pixelactions_core::display::Server;
let Ok(portal) = crate::portal::capabilities() else {
return no_input_path(Server::Wayland);
};
LinuxStatus {
rung: if portal.usable() {
"portal RemoteDesktop + EIS"
} else {
"none"
},
portal_remote_desktop_version: Some(portal.remote_desktop_version),
portal_screen_cast_version: Some(portal.screen_cast_version),
portal_device_types: Some(portal.device_types),
cursor_metadata_available: Some(portal.cursor_metadata()),
grant_remembered: Some(portal.have_stored_token),
..no_input_path(Server::Wayland)
}
}
#[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 {
use pixelactions_core::display::Server;
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),
};
}
match crate::inject::session_server() {
Server::X11 => probe_x11(),
_ => probe_wayland(),
}
}
#[cfg(target_os = "linux")]
fn probe_x11() -> Probe {
let outcome = crate::inject::X11Injector::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 probe_wayland() -> Probe {
let outcome = crate::inject::WaylandInjector::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:?}");
}
}
use pixelactions_core::display::Server;
fn x11(connected: bool) -> LinuxStatus {
LinuxStatus {
rung: if connected {
"XTEST on the root window"
} else {
"none"
},
display: Some(":0".to_string()),
connected: Some(connected),
..no_input_path(Server::X11)
}
}
fn wayland() -> LinuxStatus {
LinuxStatus {
rung: "portal RemoteDesktop + EIS",
portal_remote_desktop_version: Some(2),
portal_screen_cast_version: Some(5),
portal_device_types: Some(0b111),
cursor_metadata_available: Some(true),
grant_remembered: Some(true),
..no_input_path(Server::Wayland)
}
}
#[test]
fn an_x11_session_is_never_described_as_a_wayland_one() {
let linux = x11(true);
assert_eq!(display_line(&linux).as_deref(), Some(":0 — connected"));
assert!(
portal_line(&linux).is_none(),
"nothing asked the portal on X11, so there is no version to print"
);
let grant = grant_line(&linux);
assert!(grant.contains("none needed"), "{grant}");
let kill = kill_switch_line(&linux);
assert!(kill.starts_with("armed"), "{kill}");
assert!(
!kill.contains("no eyes"),
"X11 can read the pointer: {kill}"
);
}
#[test]
fn the_kill_switch_line_disagrees_between_the_two_servers() {
assert_ne!(
kill_switch_line(&x11(true)),
kill_switch_line(&wayland()),
"the whole point of reporting it is that the answer differs"
);
assert!(kill_switch_line(&wayland()).contains("no eyes"));
assert!(
kill_switch_line(&no_input_path(Server::Unknown)).contains("no session"),
"a session with no path has nothing to watch either"
);
}
#[test]
fn an_x_display_that_did_not_answer_says_so() {
let line = display_line(&x11(false)).expect("X11 names its display");
assert!(line.contains("no answer"), "{line}");
}
#[test]
fn a_wayland_session_still_reports_its_portal_and_grant() {
let linux = wayland();
let portal = portal_line(&linux).expect("the portal answered");
assert!(portal.contains("RemoteDesktop v2"), "{portal}");
assert!(portal.contains("ScreenCast v5"), "{portal}");
assert!(
display_line(&linux).is_none(),
"Wayland has no X display to name"
);
assert!(grant_line(&linux).contains("remembered"));
}
#[test]
fn the_capability_line_names_what_each_path_costs() {
let report = |linux: Option<LinuxStatus>| Report {
schema: 1,
platform: "linux",
supported_platform: true,
native_space: pixelactions_core::convert::Space::Physical,
session_schema_supported: SUPPORTED_SCHEMA,
pixelcoords: pixelcoords_status(),
capabilities: Capabilities {
resolve: true,
inject: true,
verify: true,
},
accessibility_trusted: None,
linux,
probe: Probe {
attempted: false,
moved: false,
confirmed: false,
detail: None,
},
};
let x11_line = inject_line(&report(Some(x11(true))));
assert!(x11_line.contains("XTEST"), "{x11_line}");
assert!(x11_line.contains("asks nothing of you"), "{x11_line}");
let wayland_line = inject_line(&report(Some(wayland())));
assert!(wayland_line.contains("remembered"), "{wayland_line}");
let dead = inject_line(&report(Some(x11(false))));
assert!(!dead.contains("via none"), "{dead}");
assert!(dead.starts_with("no"), "{dead}");
let mac_line = inject_line(&report(None));
assert!(mac_line.contains("Accessibility"), "{mac_line}");
}
#[test]
fn absent_fields_are_omitted_rather_than_zeroed() {
let json = serde_json::to_string(&x11(true)).expect("serializes");
assert!(json.contains(r#""server":"x11""#), "{json}");
assert!(json.contains(r#""connected":true"#), "{json}");
assert!(!json.contains("portal_"), "{json}");
assert!(!json.contains("grant_remembered"), "{json}");
let json = serde_json::to_string(&wayland()).expect("serializes");
assert!(
json.contains(r#""portal_remote_desktop_version":2"#),
"{json}"
);
assert!(!json.contains("connected"), "{json}");
}
}