424 lines
15 KiB
Rust
424 lines
15 KiB
Rust
//! 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<ExtOutputImageCaptureSourceManagerV1>,
|
|
pub(crate) tiles: Vec<Tile>,
|
|
|
|
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<overlay::Hover>,
|
|
pub(crate) pressed: Option<usize>,
|
|
/// 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<WpCursorShapeManagerV1>,
|
|
pub(crate) cursor_device: Option<WpCursorShapeDeviceV1>,
|
|
|
|
pub(crate) labels: Option<text::Labels>,
|
|
pub(crate) surface: Option<WlSurface>,
|
|
pub(crate) chrome: Option<shm::Chrome>,
|
|
pub(crate) chrome_buffers: Vec<WlBuffer>,
|
|
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<Target>,
|
|
pub(crate) stats: Stats,
|
|
|
|
pub(crate) seat: Option<WlSeat>,
|
|
pub(crate) inhibit_mgr: Option<ZwpKeyboardShortcutsInhibitManagerV1>,
|
|
pub(crate) inhibitor: Option<ZwpKeyboardShortcutsInhibitorV1>,
|
|
pub(crate) latched_modifiers: std::collections::BTreeSet<u32>,
|
|
|
|
/// The navigation key currently held down, and when the next repeat fires.
|
|
pub(crate) repeat_key: Option<u32>,
|
|
pub(crate) repeat_next: Option<std::time::Instant>,
|
|
/// 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<Self>,
|
|
targets: Vec<Target>,
|
|
settings: Settings,
|
|
layout: Layout,
|
|
) -> Result<Self, Box<dyn Error>> {
|
|
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::<Vec<_>>()
|
|
.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<WlRegistry, GlobalListContents> for App {
|
|
fn event(
|
|
_: &mut Self,
|
|
_: &WlRegistry,
|
|
_: <WlRegistry as Proxy>::Event,
|
|
_: &GlobalListContents,
|
|
_: &Connection,
|
|
_: &QueueHandle<Self>,
|
|
) {
|
|
}
|
|
}
|
|
|
|
impl Dispatch<ExtForeignToplevelListV1, ()> for App {
|
|
fn event(
|
|
app: &mut Self,
|
|
_: &ExtForeignToplevelListV1,
|
|
event: ext_foreign_toplevel_list_v1::Event,
|
|
_: &(),
|
|
_: &Connection,
|
|
_: &QueueHandle<Self>,
|
|
) {
|
|
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<ExtForeignToplevelHandleV1, ()> for App {
|
|
fn event(
|
|
app: &mut Self,
|
|
handle: &ExtForeignToplevelHandleV1,
|
|
event: ext_foreign_toplevel_handle_v1::Event,
|
|
_: &(),
|
|
_: &Connection,
|
|
_: &QueueHandle<Self>,
|
|
) {
|
|
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<WlOutput, ()> for App {
|
|
fn event(
|
|
app: &mut Self,
|
|
output: &WlOutput,
|
|
event: wl_output::Event,
|
|
_: &(),
|
|
_: &Connection,
|
|
_: &QueueHandle<Self>,
|
|
) {
|
|
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();
|
|
}
|
|
}
|
|
}
|