mod host;
mod pip;
use clap::Parser;
use std::path::PathBuf;
use std::sync::mpsc;
use std::time::{Duration, Instant};
#[derive(Parser)]
#[command(name = "wlr-pip", version, about)]
struct Cli {
identifier: Option<String>,
#[arg(long, value_name = "SECS", hide = true)]
bench: Option<u64>,
#[arg(long, hide = true)]
list: bool,
}
fn main() {
let cli = Cli::parse();
wlr_capture::i18n::init();
if cli.list {
list_windows();
return;
}
let identifier = match cli.identifier {
Some(id) => id,
None => match pick_via_chooser() {
Some(id) => id,
None => std::process::exit(1), },
};
if let Some(secs) = cli.bench {
bench(identifier, secs);
return;
}
let _lock = match acquire_pip_lock(&identifier) {
Some(lock) => lock,
None => return, };
let (label, icon) = resolve_window(&identifier);
if let Err(e) = host::run(identifier, label, icon) {
eprintln!("wlr-pip: {e:#}");
std::process::exit(2);
}
}
fn pick_via_chooser() -> Option<String> {
let sibling = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|d| d.join("wlr-chooser")))
.filter(|p| p.exists());
let mut cmd = match sibling {
Some(p) => std::process::Command::new(p),
None => std::process::Command::new("wlr-chooser"),
};
let out = cmd.arg("--windows").output().ok()?;
if !out.status.success() {
return None;
}
String::from_utf8_lossy(&out.stdout)
.lines()
.find_map(|l| l.strip_prefix("Window: ").map(|id| id.trim().to_string()))
.filter(|id| !id.is_empty())
}
fn acquire_pip_lock(identifier: &str) -> Option<std::fs::File> {
use rustix::fs::{FlockOperation, flock};
let dir = std::env::var_os("XDG_RUNTIME_DIR")
.map(PathBuf::from)
.unwrap_or_else(std::env::temp_dir);
let safe: String = identifier
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect();
let f = std::fs::OpenOptions::new()
.create(true)
.write(true)
.truncate(false)
.open(dir.join(format!("wlr-pip-{safe}.lock")))
.ok()?;
flock(&f, FlockOperation::NonBlockingLockExclusive).ok()?;
Some(f)
}
fn resolve_window(identifier: &str) -> (String, Option<(u32, u32, Vec<u8>)>) {
let mut client = match wlr_capture::wl::Client::connect() {
Ok(c) => c,
Err(_) => return ("wlr-pip".to_string(), None),
};
let _ = client.refresh();
let Some(t) = client
.toplevels()
.iter()
.find(|t| t.identifier == identifier)
else {
return ("wlr-pip".to_string(), None);
};
let label = if t.app_id.is_empty() {
t.title.clone()
} else if t.title.is_empty() {
t.app_id.clone()
} else {
format!("{} — {}", t.app_id, t.title)
};
let icon =
wlr_capture::icons::resolve(&t.app_id).and_then(|p| wlr_capture::icons::load(&p, 64));
(label, icon)
}
fn list_windows() {
let mut client = match wlr_capture::wl::Client::connect() {
Ok(c) => c,
Err(e) => {
eprintln!("wlr-pip: {e:#}");
std::process::exit(2);
}
};
let _ = client.refresh();
for t in client.toplevels() {
println!("{}\t{}\t{}", t.identifier, t.app_id, t.title);
}
}
fn bench(identifier: String, secs: u64) {
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || pip::capture_thread(identifier, move |m| tx.send(m).is_ok()));
let deadline = Instant::now() + Duration::from_secs(secs);
let (mut shm, mut dmabuf) = (0u32, 0u32);
while Instant::now() < deadline {
match rx.recv_timeout(Duration::from_millis(200)) {
Ok(pip::PipMsg::Shm { w, h, .. }) => {
shm += 1;
if shm == 1 {
eprintln!("bench: first shm frame {w}x{h}");
}
}
Ok(pip::PipMsg::Dmabuf { frame }) => {
dmabuf += 1;
if dmabuf == 1 {
eprintln!(
"bench: first dma-buf frame {}x{}",
frame.width, frame.height
);
}
}
Ok(pip::PipMsg::Gone) => {
eprintln!("bench: source gone");
break;
}
Err(mpsc::RecvTimeoutError::Timeout) => {}
Err(mpsc::RecvTimeoutError::Disconnected) => break,
}
}
eprintln!("bench: {shm} shm frame(s), {dmabuf} dma-buf frame(s) in {secs}s");
}