use crate::error::{Context, Result};
use crate::wl;
const CHECKS: &[(&str, &str)] = &[
("ext_image_copy_capture_manager_v1", "capture frames (core)"),
(
"ext_output_image_capture_source_manager_v1",
"capture an output (core)",
),
(
"ext_foreign_toplevel_image_capture_source_manager_v1",
"capture a window",
),
(
"ext_foreign_toplevel_list_v1",
"enumerate windows (chooser, -w)",
),
("zxdg_output_manager_v1", "accurate output geometry"),
(
"zwlr_layer_shell_v1",
"overlays: region select, loupe, switcher",
),
("zwlr_data_control_manager_v1", "clipboard copy (-c)"),
("zwp_linux_dmabuf_v1", "zero-copy GPU capture"),
(
"zwp_keyboard_shortcuts_inhibit_manager_v1",
"switcher keyboard grab",
),
];
pub fn capture_verdict(globals: &[(String, u32)]) -> (bool, bool) {
let has = |iface: &str| globals.iter().any(|(n, _)| n == iface);
let screen = has("ext_image_copy_capture_manager_v1")
&& has("ext_output_image_capture_source_manager_v1");
let window = has("ext_foreign_toplevel_image_capture_source_manager_v1");
(screen, window)
}
pub fn report(tool: &str, version: &str) -> Result<()> {
println!("{tool} {version}\n");
if let Some(os) = os_pretty_name() {
println!(" OS: {os}");
}
println!(" Compositor: {}", compositor());
if let Ok(exe) = std::env::current_exe() {
let home = std::env::var("HOME").ok();
println!(
" Executable: {}",
redact_home(&exe.display().to_string(), home.as_deref())
);
}
println!();
let globals = wl::advertised_globals().context("listing Wayland globals")?;
let global_version = |iface: &str| globals.iter().find(|(n, _)| n == iface).map(|(_, v)| *v);
println!("Compositor capabilities (advertised Wayland globals):\n");
for (iface, desc) in CHECKS {
match global_version(iface) {
Some(v) => println!(" ✓ {iface} (v{v}) — {desc}"),
None => println!(" ✗ {iface} — {desc}"),
}
}
let (core, window) = capture_verdict(&globals);
println!();
if core {
println!(
"Screen capture: supported (screenshots, recording, loupe, colour picker, wlr-draw)."
);
} else {
println!(
"Screen capture: UNSUPPORTED — needs ext-image-copy-capture-v1 + the output \
source (wlroots ≥ 0.19 / Sway ≥ 1.11; not on Mutter/KWin via this path)."
);
}
if window {
println!(
"Window capture: supported (wlr-switcher, -w/--pick-window, window mirror/record)."
);
} else {
println!(
"Window capture: UNSUPPORTED — needs the foreign-toplevel source \
(wlroots ≥ 0.20 / Sway ≥ 1.12). Screen capture still works; only window-only \
features are unavailable (wlr-switcher exits with a notice)."
);
}
#[cfg(feature = "focus")]
match crate::focus::detect() {
Some(b) => println!(
"Focus IPC: {} detected (-a / --current-output work).",
b.name()
),
None => println!("Focus IPC: none detected (-a / --current-output unavailable)."),
}
Ok(())
}
fn redact_home(path: &str, home: Option<&str>) -> String {
if let Some(rest) = home
.filter(|h| !h.is_empty())
.and_then(|h| path.strip_prefix(h))
.filter(|r| r.is_empty() || r.starts_with('/'))
{
return format!("~{rest}");
}
path.to_string()
}
fn os_pretty_name() -> Option<String> {
parse_pretty_name(&std::fs::read_to_string("/etc/os-release").ok()?)
}
fn parse_pretty_name(os_release: &str) -> Option<String> {
os_release
.lines()
.find_map(|l| l.strip_prefix("PRETTY_NAME="))
.map(|v| v.trim_matches('"').to_string())
}
fn compositor() -> String {
match detected_name() {
None => "unknown (XDG_CURRENT_DESKTOP unset or unrecognised)".to_string(),
Some(name) => match self_reported_version(&name) {
Some(v) => format!("{name} — {v}"),
None => name,
},
}
}
fn detected_name() -> Option<String> {
#[cfg(feature = "focus")]
if let Some(b) = crate::focus::detect() {
return Some(b.name().to_string());
}
std::env::var("XDG_CURRENT_DESKTOP")
.or_else(|_| std::env::var("XDG_SESSION_DESKTOP"))
.ok()
.filter(|s| !s.is_empty())
}
fn self_reported_version(name: &str) -> Option<String> {
let n = name.to_ascii_lowercase();
let (cmd, args): (&str, &[&str]) = if n.contains("sway") {
("sway", &["--version"])
} else if n.contains("hypr") {
("hyprctl", &["version"])
} else if n.contains("niri") {
("niri", &["--version"])
} else {
return None;
};
let out = std::process::Command::new(cmd).args(args).output().ok()?;
String::from_utf8_lossy(&out.stdout)
.lines()
.map(str::trim)
.find(|l| !l.is_empty())
.map(String::from)
}
#[cfg(test)]
mod tests {
use super::{capture_verdict, parse_pretty_name, redact_home};
#[test]
fn redact_home_replaces_the_home_dir_with_tilde() {
assert_eq!(
redact_home("/home/bob/.cargo/bin/wlr-peek", Some("/home/bob")),
"~/.cargo/bin/wlr-peek"
);
assert_eq!(
redact_home("/root/.cargo/bin/wlr-peek", Some("/root")),
"~/.cargo/bin/wlr-peek"
);
assert_eq!(redact_home("/home/bob", Some("/home/bob")), "~");
assert_eq!(
redact_home("/home/bobby/x", Some("/home/bob")),
"/home/bobby/x"
);
assert_eq!(redact_home("/home/alice/x", None), "/home/alice/x");
assert_eq!(
redact_home("/usr/bin/wlr-draw", Some("/home/bob")),
"/usr/bin/wlr-draw"
);
}
#[test]
fn parse_pretty_name_reads_the_quoted_value() {
let sample = "NAME=\"Debian GNU/Linux\"\n\
PRETTY_NAME=\"Debian GNU/Linux 13 (trixie)\"\n\
VERSION_ID=\"13\"\n";
assert_eq!(
parse_pretty_name(sample).as_deref(),
Some("Debian GNU/Linux 13 (trixie)")
);
assert_eq!(parse_pretty_name("ID=arch\n"), None);
}
fn globals(names: &[&str]) -> Vec<(String, u32)> {
names.iter().map(|n| ((*n).to_string(), 1)).collect()
}
#[test]
fn capture_verdict_reads_screen_and_window_floors() {
const CORE: [&str; 2] = [
"ext_image_copy_capture_manager_v1",
"ext_output_image_capture_source_manager_v1",
];
const FOREIGN: &str = "ext_foreign_toplevel_image_capture_source_manager_v1";
assert_eq!(capture_verdict(&globals(&[])), (false, false));
assert_eq!(capture_verdict(&globals(&CORE)), (true, false));
let mut all = CORE.to_vec();
all.push(FOREIGN);
assert_eq!(capture_verdict(&globals(&all)), (true, true));
assert_eq!(capture_verdict(&globals(&CORE[..1])), (false, false));
assert_eq!(capture_verdict(&globals(&[FOREIGN])), (false, true));
}
}