//! The client: everything the compositor talks to us through. //! //! `App` is the one state the Wayland event queue dispatches into, so it holds //! the bound globals, the tiles, and the pieces of the overlay. The work itself //! lives next door: capture.rs drives the sessions, overlay.rs draws and reads //! the keyboard. use std::error::Error; use wayland_client::globals::{GlobalList, GlobalListContents}; use wayland_client::protocol::{ wl_buffer::WlBuffer, wl_compositor::WlCompositor, wl_output::{self, WlOutput}, wl_registry::WlRegistry, wl_seat::WlSeat, wl_shm::WlShm, wl_shm_pool::WlShmPool, wl_subcompositor::WlSubcompositor, wl_subsurface::WlSubsurface, wl_surface::WlSurface, }; use wayland_client::{ Connection, Dispatch, Proxy, QueueHandle, delegate_noop, event_created_child, }; use wayland_protocols::ext::foreign_toplevel_list::v1::client::{ ext_foreign_toplevel_handle_v1::{self, ExtForeignToplevelHandleV1}, ext_foreign_toplevel_list_v1::{self, ExtForeignToplevelListV1}, }; use wayland_protocols::ext::image_capture_source::v1::client::{ ext_foreign_toplevel_image_capture_source_manager_v1::ExtForeignToplevelImageCaptureSourceManagerV1, ext_image_capture_source_v1::ExtImageCaptureSourceV1, ext_output_image_capture_source_manager_v1::ExtOutputImageCaptureSourceManagerV1, }; use wayland_protocols::ext::image_copy_capture::v1::client::ext_image_copy_capture_manager_v1::ExtImageCopyCaptureManagerV1; use wayland_protocols::wp::cursor_shape::v1::client::{ wp_cursor_shape_device_v1::WpCursorShapeDeviceV1, wp_cursor_shape_manager_v1::WpCursorShapeManagerV1, }; use wayland_protocols::wp::keyboard_shortcuts_inhibit::zv1::client::{ zwp_keyboard_shortcuts_inhibit_manager_v1::ZwpKeyboardShortcutsInhibitManagerV1, zwp_keyboard_shortcuts_inhibitor_v1::ZwpKeyboardShortcutsInhibitorV1, }; use wayland_protocols::wp::viewporter::client::{ wp_viewport::WpViewport, wp_viewporter::WpViewporter, }; use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_shell_v1::ZwlrLayerShellV1; use crate::capture::Tile; use crate::overlay; use crate::shm; use crate::target::Target; use crate::text; use crate::theme::{Layout, Theme}; /// What the caller decided before any of this started: the look, and how much /// live capture to do. `live` is always on for the alt-tab switcher mode. pub struct Settings { pub theme: Theme, pub fps: u32, /// Integer scale of the display the overlay renders on, and its name, so /// the overlay maps there rather than wherever the compositor would put it. pub scale: i32, pub output: String, } pub struct App { pub(crate) compositor: WlCompositor, pub(crate) subcompositor: WlSubcompositor, pub(crate) shm: WlShm, pub(crate) viewporter: WpViewporter, pub(crate) layer_shell: ZwlrLayerShellV1, pub(crate) copy_mgr: ExtImageCopyCaptureManagerV1, pub(crate) src_mgr: ExtForeignToplevelImageCaptureSourceManagerV1, /// Toplevel handles as the compositor announces them, paired with the /// identifier that joins them to sway's tree. pub(crate) toplevels: Vec<(ExtForeignToplevelHandleV1, String)>, /// Displays, paired with the name the compositor gives them (wl_output v4). pub(crate) outputs: Vec<(WlOutput, String)>, pub(crate) output_src_mgr: Option, pub(crate) tiles: Vec, pub(crate) theme: Theme, pub(crate) layout: Layout, pub(crate) fps: u32, pub(crate) scale: i32, pub(crate) sel: usize, /// First row of the grid on screen. The rest scroll. pub(crate) scroll: i32, pub(crate) shift: bool, /// Where the pointer is, and which tile it pressed. Hovering deliberately /// does not move the keyboard selection; a click acts on what is under the /// cursor instead. pub(crate) hover: Option, pub(crate) pressed: Option, /// Set the cursor shape without shipping a cursor theme. Optional: without /// it the pointer keeps whatever shape it had over the window below. pub(crate) cursor_shape: Option, pub(crate) cursor_device: Option, pub(crate) labels: Option, pub(crate) surface: Option, pub(crate) chrome: Option, pub(crate) chrome_buffers: Vec, pub(crate) configured: bool, /// The display the overlay maps on, by name. pub(crate) output: String, pub(crate) ending: Ending, /// Whether we hold the keyboard. Without it the overlay cannot be operated. pub(crate) focused: bool, pub(crate) picked: Option, pub(crate) stats: Stats, pub(crate) seat: Option, pub(crate) inhibit_mgr: Option, pub(crate) inhibitor: Option, pub(crate) latched_modifiers: std::collections::BTreeSet, /// The navigation key currently held down, and when the next repeat fires. pub(crate) repeat_key: Option, pub(crate) repeat_next: Option, /// Milliseconds before the first repeat fires. From wl_keyboard::RepeatInfo. pub(crate) repeat_delay_ms: u32, /// Milliseconds between subsequent repeats. From wl_keyboard::RepeatInfo. pub(crate) repeat_rate_ms: u32, } /// Counters worth reporting with --verbose. Live capture is easy to get subtly /// wrong - a starved buffer pool or a clock that never ticks both look like /// "nothing updates" - so the numbers that distinguish those stay available. #[derive(Default)] pub struct Stats { /// Frame callbacks received, i.e. how often the live clock fired. pub(crate) ticks: u32, /// Re-captures skipped because every buffer was still held. pub(crate) starved: u32, /// Bytes of shm handed to the compositor for capture buffers. pub(crate) pool_bytes: usize, } /// Why the overlay stopped, for --verbose. An exit status of 1 cannot tell a /// cancel from a surface the compositor took away. #[derive(Clone, Copy, PartialEq, Eq)] pub enum Ending { Running, Picked, Cancelled, Closed, /// The keyboard went to another surface, so we can no longer be operated. Unfocused, } impl Ending { pub fn as_str(self) -> &'static str { match self { Ending::Running => "still running", Ending::Picked => "picked", Ending::Cancelled => "cancelled", Ending::Closed => "the compositor closed the overlay", Ending::Unfocused => "lost the keyboard to another surface", } } } impl App { pub fn new( globals: &GlobalList, qh: &QueueHandle, targets: Vec, settings: Settings, layout: Layout, ) -> Result> { let Settings { theme, fps, scale, output, .. } = settings; let sel = if targets.len() > 1 { 1 } else { 0 }; // Bind everything up front so a compositor missing a protocol fails // here, with a name, rather than halfway through a capture. let mut app = Self { compositor: globals.bind(qh, 1..=6, ())?, subcompositor: globals.bind(qh, 1..=1, ())?, shm: globals.bind(qh, 1..=1, ())?, viewporter: globals.bind(qh, 1..=1, ())?, layer_shell: globals.bind(qh, 1..=5, ())?, copy_mgr: globals.bind(qh, 1..=1, ())?, src_mgr: globals.bind(qh, 1..=1, ())?, toplevels: Vec::new(), outputs: Vec::new(), // Optional: a compositor without it simply gets no display tiles. output_src_mgr: globals.bind(qh, 1..=1, ()).ok(), tiles: targets.into_iter().map(Tile::new).collect(), theme, layout, fps, scale, sel, scroll: 0, shift: false, hover: None, pressed: None, cursor_shape: globals.bind(qh, 1..=2, ()).ok(), cursor_device: None, labels: None, surface: None, chrome: None, chrome_buffers: Vec::new(), configured: false, output, ending: Ending::Running, focused: false, picked: None, stats: Stats::default(), seat: None, inhibit_mgr: None, inhibitor: None, latched_modifiers: std::collections::BTreeSet::new(), repeat_key: None, repeat_next: None, repeat_delay_ms: 600, repeat_rate_ms: 25, }; let _: ExtForeignToplevelListV1 = globals.bind(qh, 1..=1, ())?; // One wl_output per display, bound at v4 so it tells us its name. for global in globals.contents().clone_list() { if global.interface == WlOutput::interface().name { let version = global.version.min(4); if version >= 4 { let output: WlOutput = globals.registry().bind(global.name, version, qh, ()); app.outputs.push((output, String::new())); } } } let seat: WlSeat = globals.bind(qh, 1..=7, ())?; app.seat = Some(seat); app.inhibit_mgr = globals.bind(qh, 1..=1, ()).ok(); Ok(app) } pub fn finished(&self) -> bool { self.ending != Ending::Running } /// The pick, if there was one. pub fn picked(&self) -> Option<&Target> { self.picked.as_ref() } /// What the grid is about to show: one line per tile, then the geometry. pub fn describe(&self) { for (i, t) in self.tiles.iter().enumerate() { let mark = if t.ready { "" } else { " (no thumbnail)" }; eprintln!(" [{i}] {}{mark}", t.target.tsv()); } eprintln!( "wl-pick: {} tile(s), {} captured; grid {}x{}, surface {}x{} logical \ at scale {}, {} MB of capture buffers, labels in {:?}", self.tiles.len(), self.tiles.iter().filter(|t| t.ready).count(), self.layout.cols, self.layout.rows, self.layout.width, self.layout.height, self.scale, self.stats.pool_bytes >> 20, self.labels.as_ref().map(|l| l.family()).unwrap_or("none"), ); if self.layout.scrollable() { eprintln!( "wl-pick: {} of {} rows fit; the rest scroll", self.layout.visible_rows, self.layout.rows ); } } /// What live capture actually did, once the overlay is closing. pub fn report(&self, open_for: std::time::Duration) { let frames: u32 = self.tiles.iter().map(|t| t.frames).sum(); let secs = open_for.as_secs_f64(); eprintln!( "wl-pick: {}, {frames} frame(s) over {secs:.1}s = {:.1}/s, {} tick(s), \ {} starved; per tile: {}", self.ending.as_str(), frames as f64 / secs, self.stats.ticks, self.stats.starved, self.tiles .iter() .map(|t| t.frames.to_string()) .collect::>() .join(",") ); } pub fn captures_settled(&self) -> bool { self.tiles.iter().all(|t| t.settled) } /// Say which tiles the compositor went quiet on, and name the likeliest /// reason: sway answers a capture request on a toplevel that another client /// is already capturing with silence rather than with `failed`. pub fn report_unsettled(&self) { let stuck: Vec<&str> = self .tiles .iter() .filter(|t| !t.settled) .map(|t| t.target.title.as_str()) .collect(); eprintln!( "wl-pick: no frame for {} of {} tiles ({}); \ another capture client may hold these sources", stuck.len(), self.tiles.len(), stuck.join(", ") ); } } // --- enumeration ---------------------------------------------------------- impl Dispatch for App { fn event( _: &mut Self, _: &WlRegistry, _: ::Event, _: &GlobalListContents, _: &Connection, _: &QueueHandle, ) { } } impl Dispatch for App { fn event( app: &mut Self, _: &ExtForeignToplevelListV1, event: ext_foreign_toplevel_list_v1::Event, _: &(), _: &Connection, _: &QueueHandle, ) { if let ext_foreign_toplevel_list_v1::Event::Toplevel { toplevel } = event { app.toplevels.push((toplevel, String::new())); } } event_created_child!(App, ExtForeignToplevelListV1, [ ext_foreign_toplevel_list_v1::EVT_TOPLEVEL_OPCODE => (ExtForeignToplevelHandleV1, ()), ]); } impl Dispatch for App { fn event( app: &mut Self, handle: &ExtForeignToplevelHandleV1, event: ext_foreign_toplevel_handle_v1::Event, _: &(), _: &Connection, _: &QueueHandle, ) { if let ext_foreign_toplevel_handle_v1::Event::Identifier { identifier } = event && let Some(entry) = app.toplevels.iter_mut().find(|(h, _)| h == handle) { entry.1 = identifier; } } } /// wl_output tells us its name (v4), which is how a display tile is labelled /// and how `focus output NAME` finds it again. impl Dispatch for App { fn event( app: &mut Self, output: &WlOutput, event: wl_output::Event, _: &(), _: &Connection, _: &QueueHandle, ) { if let wl_output::Event::Name { name } = event && let Some(entry) = app.outputs.iter_mut().find(|(o, _)| o == output) { entry.1 = name; } } } // Interfaces we drive but never listen to. delegate_noop!(App: WlCompositor); delegate_noop!(App: WlSubcompositor); delegate_noop!(App: WlSubsurface); delegate_noop!(App: ignore WlShm); delegate_noop!(App: WlShmPool); delegate_noop!(App: WpViewporter); delegate_noop!(App: WpViewport); delegate_noop!(App: ZwlrLayerShellV1); delegate_noop!(App: ExtImageCopyCaptureManagerV1); delegate_noop!(App: ExtForeignToplevelImageCaptureSourceManagerV1); delegate_noop!(App: ExtImageCaptureSourceV1); delegate_noop!(App: ExtOutputImageCaptureSourceManagerV1); delegate_noop!(App: ignore WlSurface); // The chrome's own buffers: two slots alternating on keypresses, so their // release timing does not matter. delegate_noop!(App: WpCursorShapeManagerV1); delegate_noop!(App: WpCursorShapeDeviceV1); delegate_noop!(App: ignore WlBuffer); delegate_noop!(App: ZwpKeyboardShortcutsInhibitManagerV1); delegate_noop!(App: ignore ZwpKeyboardShortcutsInhibitorV1); impl Drop for App { fn drop(&mut self) { if let Some(inhibitor) = self.inhibitor.take() { inhibitor.destroy(); } } }