#![allow(clippy::chunks_exact_to_as_chunks)]
mod app;
mod capture;
mod cli;
mod config;
mod overlay;
mod shm;
mod sway;
mod target;
mod text;
mod theme;
use std::error::Error;
use std::process::ExitCode;
use std::time::{Duration, Instant};
use rustix::event::{PollFd, PollFlags, Timespec};
use wayland_client::globals::registry_queue_init;
use wayland_client::{Connection, EventQueue};
use app::{App, Ending};
use config::Config;
use target::Target;
use theme::Layout;
const STARTUP_BUDGET: Duration = Duration::from_secs(2);
const REFOCUS_GRACE: Duration = Duration::from_millis(150);
fn main() -> ExitCode {
match run() {
Ok(code) => code,
Err(e) => {
eprintln!("wl-pick: {e}");
ExitCode::FAILURE
}
}
}
fn run() -> Result<ExitCode, Box<dyn Error>> {
let args = cli::parse_args().map_err(Box::<dyn Error>::from)?;
let config = Config::load(args.config.as_deref()).map_err(Box::<dyn Error>::from)?;
let start = Instant::now();
let mut phases = Phases::new(args.verbose);
let (targets, opts) = {
let mut sway = sway::connect()?;
let displays = sway::displays(&mut sway)?;
let display = sway::focused(&displays).ok_or("sway reports no active display")?;
let opts = args.resolve(&config, display);
let mut targets = sway::windows(&mut sway)?;
if opts.outputs {
targets.extend(displays.iter().map(|d| Target::output(d.name.clone())));
}
(targets, opts)
};
if targets.is_empty() {
return Ok(ExitCode::FAILURE);
}
phases.mark("sway-tree");
cli::arm_timeout(opts.timeout);
let (display, settings) = (opts.display, opts.settings);
let theme = &settings.theme;
let scale = settings.scale;
let layout = Layout::new(theme, targets.len() as i32, display);
let labels = layout.label(0, 0).map(|label| {
text::spawn(
targets.iter().map(Target::label).collect(),
theme.font.clone(),
theme.font_px * scale as f32,
(theme.line_h * scale) as f32,
(label.w * scale) as f32,
)
});
let conn = Connection::connect_to_env()?;
let (globals, mut queue) = registry_queue_init::<App>(&conn)?;
let qh = queue.handle();
let mut app = App::new(&globals, &qh, targets, settings, layout)?;
queue.roundtrip(&mut app)?;
queue.roundtrip(&mut app)?;
phases.mark("toplevels");
app.open_sessions(&qh);
queue.roundtrip(&mut app)?; phases.mark("constraints");
app.start_captures(&qh)?;
if !pump_for(
&conn,
&mut queue,
&mut app,
|a| a.captures_settled(),
STARTUP_BUDGET,
)? {
app.report_unsettled();
}
phases.mark("capture");
if let Some(job) = labels {
app.labels = Some(job.join().map_err(|_| "label thread panicked")?);
}
phases.mark("labels");
if opts.verbose {
app.describe();
}
app.show(&qh)?;
if !pump_for(
&conn,
&mut queue,
&mut app,
|a| a.configured,
STARTUP_BUDGET,
)? {
return Err("the compositor never configured the overlay".into());
}
app.paint();
app.sync_tiles(&qh);
app.arm_frame_callback(&qh);
conn.flush()?;
phases.mark("mapped");
pump_interactive(&conn, &mut queue, &mut app)?;
if opts.verbose {
app.report(start.elapsed());
}
let Some(target) = app.picked() else {
return Ok(ExitCode::FAILURE); };
match target.render(opts.format) {
Some(line) => println!("{line}"),
None => {
eprintln!("wl-pick: {:?} has no toplevel identifier", target.title);
return Ok(ExitCode::FAILURE);
}
}
Ok(ExitCode::SUCCESS)
}
fn pump_interactive(
conn: &Connection,
queue: &mut EventQueue<App>,
app: &mut App,
) -> Result<(), Box<dyn Error>> {
while !app.finished() {
queue.blocking_dispatch(app)?;
if app.finished() || app.focused {
continue;
}
if !pump_for(
conn,
queue,
app,
|a| a.focused || a.finished(),
REFOCUS_GRACE,
)? {
app.ending = Ending::Unfocused;
}
}
Ok(())
}
fn pump_for(
conn: &Connection,
queue: &mut EventQueue<App>,
app: &mut App,
done: impl Fn(&App) -> bool,
limit: Duration,
) -> Result<bool, Box<dyn Error>> {
let deadline = Instant::now() + limit;
loop {
queue.dispatch_pending(app)?;
if done(app) {
return Ok(true);
}
conn.flush()?;
let Some(guard) = conn.prepare_read() else {
continue;
};
let Some(left) = deadline.checked_duration_since(Instant::now()) else {
return Ok(false);
};
let fd = guard.connection_fd();
let mut fds = [PollFd::new(&fd, PollFlags::IN)];
let timeout = Timespec {
tv_sec: left.as_secs() as _,
tv_nsec: left.subsec_nanos() as _,
};
match rustix::event::poll(&mut fds, Some(&timeout)) {
Ok(0) => return Ok(false),
Ok(_) | Err(rustix::io::Errno::INTR) => {}
Err(e) => return Err(Box::new(e)),
}
guard.read()?;
}
}
struct Phases {
on: bool,
last: Instant,
}
impl Phases {
fn new(on: bool) -> Self {
Self {
on,
last: Instant::now(),
}
}
fn mark(&mut self, label: &str) {
if self.on {
let now = Instant::now();
eprintln!(
"{label:<12} {:6.1}ms",
(now - self.last).as_secs_f64() * 1000.0
);
self.last = now;
}
}
}