AnyBot/src/wl.rs

670 lines
27 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Linux Wayland on wlroots desktops (sway, Hyprland, river, labwc, Wayfire…), pure Rust through
// the compositor's own protocols:
// screenshots wlr-screencopy (every screen, placed where it is in the layout)
// mouse wlr-virtual-pointer (absolute moves over the whole layout)
// keyboard virtual-keyboard: AnyBot hands the compositor a small keymap with exactly the
// keys it needs, so any character (Chinese too) types directly, like `wtype`
// windows wlr-foreign-toplevel (which app is in front; Wayland gives no window places)
// idle time ext-idle-notify (a thread that hears "idle" / "back")
// GNOME and KDE don't offer these to apps; desk.rs uses the desktop portals there (portal.rs).
use std::io::Write as _;
use std::os::fd::AsFd;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use wayland_client::globals::{registry_queue_init, GlobalListContents};
use wayland_client::protocol::{wl_buffer, wl_output, wl_pointer, wl_registry, wl_seat, wl_shm, wl_shm_pool};
use wayland_client::{delegate_noop, event_created_child, Connection, Dispatch, EventQueue, Proxy, QueueHandle, WEnum};
use wayland_protocols::ext::idle_notify::v1::client::{ext_idle_notification_v1, ext_idle_notifier_v1};
use wayland_protocols::xdg::xdg_output::zv1::client::{zxdg_output_manager_v1, zxdg_output_v1};
use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::{zwp_virtual_keyboard_manager_v1, zwp_virtual_keyboard_v1};
use wayland_protocols_wlr::foreign_toplevel::v1::client::{zwlr_foreign_toplevel_handle_v1 as top_handle, zwlr_foreign_toplevel_manager_v1 as top_manager};
use wayland_protocols_wlr::screencopy::v1::client::{zwlr_screencopy_frame_v1 as frame_v1, zwlr_screencopy_manager_v1};
use wayland_protocols_wlr::virtual_pointer::v1::client::{zwlr_virtual_pointer_manager_v1, zwlr_virtual_pointer_v1};
#[derive(Default, Clone)]
struct Out {
name: u32,
out: Option<wl_output::WlOutput>,
// Logical place and size (xdg-output when there is one, else from the mode and scale).
x: i32,
y: i32,
lw: i32,
lh: i32,
logical: bool,
mode_w: i32,
mode_h: i32,
scale: i32,
}
impl Out {
fn rect(&self) -> (i32, i32, i32, i32) {
if self.logical && self.lw > 0 {
(self.x, self.y, self.lw, self.lh)
} else {
let s = self.scale.max(1);
(self.x, self.y, self.mode_w / s, self.mode_h / s)
}
}
}
struct Top {
id: wayland_client::backend::ObjectId,
app: String,
active: bool,
}
#[derive(Default)]
struct Frame {
buffer: Option<(WEnum<wl_shm::Format>, u32, u32, u32)>,
buffer_done: bool,
ready: bool,
failed: bool,
y_invert: bool,
}
#[derive(Default)]
struct State {
outs: Vec<Out>,
tops: Vec<Top>,
frame: Frame,
}
struct Wl {
conn: Connection,
queue: EventQueue<State>,
qh: QueueHandle<State>,
st: State,
shm: wl_shm::WlShm,
copy: zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1,
copy_version: u32,
pointer: zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1,
kb: zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1,
/// Keysyms on our keymap: index i is evdev key i + 1 (xkb keycode i + 9).
keys: Vec<u32>,
mods: u32,
start: Instant,
}
static WL: Mutex<Option<Wl>> = Mutex::new(None);
/// The four modifiers always sit on the first keys, with their xkb modifier bits.
const MODS: [(u32, u32, &str); 4] = [(0xffe1, 1, "Shift"), (0xffe3, 4, "Control"), (0xffe9, 8, "Mod1"), (0xffeb, 64, "Mod4")];
/// Ask the kernel to give up a read on the compositor socket after `t`. A compositor that
/// stops answering used to block `blocking_dispatch` forever while the app's global Wayland
/// lock is held — every screen action (and the UI on top of it) froze. The timed-out read
/// arrives as an error, so the connection is treated as dead and rebuilt on the next call.
fn set_read_timeout(fd: std::os::fd::RawFd, t: Duration) -> std::io::Result<()> {
let tv = libc::timeval { tv_sec: i64::try_from(t.as_secs()).unwrap_or(i64::MAX), tv_usec: 0 };
let rc = unsafe {
libc::setsockopt(fd, libc::SOL_SOCKET, libc::SO_RCVTIMEO, &tv as *const _ as *const libc::c_void, std::mem::size_of::<libc::timeval>() as libc::socklen_t)
};
if rc == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
fn connect() -> Option<Wl> {
let conn = Connection::connect_to_env().ok()?;
// The compositor's socket: let the kernel give up on reads after 60 s (see
// set_read_timeout). Timed-out reads arrive as errors, so the connection is treated
// as dead and rebuilt on the next call.
use std::os::fd::AsRawFd as _;
set_read_timeout(conn.as_fd().as_raw_fd(), Duration::from_secs(60)).ok()?;
let (globals, mut queue) = registry_queue_init::<State>(&conn).ok()?;
let qh = queue.handle();
let shm: wl_shm::WlShm = globals.bind(&qh, 1..=1, ()).ok()?;
let seat: wl_seat::WlSeat = globals.bind(&qh, 1..=1, ()).ok()?;
let copy: zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1 = globals.bind(&qh, 1..=3, ()).ok()?;
let copy_version = copy.version();
let pm: zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1 = globals.bind(&qh, 1..=2, ()).ok()?;
let km: zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1 = globals.bind(&qh, 1..=1, ()).ok()?;
let pointer = pm.create_virtual_pointer(Some(&seat), &qh, ());
let kb = km.create_virtual_keyboard(&seat, &qh, ());
let xdg: Option<zxdg_output_manager_v1::ZxdgOutputManagerV1> = globals.bind(&qh, 1..=3, ()).ok();
let _tops: Option<top_manager::ZwlrForeignToplevelManagerV1> = globals.bind(&qh, 1..=3, ()).ok();
let mut st = State::default();
for g in globals.contents().clone_list() {
if g.interface == "wl_output" {
let out: wl_output::WlOutput = globals.registry().bind(g.name, g.version.min(4), &qh, g.name);
if let Some(x) = &xdg {
x.get_xdg_output(&out, &qh, g.name);
}
st.outs.push(Out { name: g.name, out: Some(out), scale: 1, ..Default::default() });
}
}
queue.roundtrip(&mut st).ok()?;
queue.roundtrip(&mut st).ok()?;
if st.outs.is_empty() {
return None;
}
let mut wl = Wl { conn, queue, qh, st, shm, copy, copy_version, pointer, kb, keys: MODS.iter().map(|m| m.0).collect(), mods: 0, start: Instant::now() };
wl.upload_keymap().ok()?;
Some(wl)
}
fn with<T>(f: impl FnOnce(&mut Wl) -> Option<T>) -> Option<T> {
let mut g = WL.lock().unwrap_or_else(|e| e.into_inner());
if g.is_none() {
*g = connect();
}
let wl = g.as_mut()?;
let _ = wl.queue.dispatch_pending(&mut wl.st);
let r = f(wl);
if wl.conn.flush().is_err() || wl.queue.roundtrip(&mut wl.st).is_err() {
// The compositor went away: connect again next time.
*g = None;
}
r
}
/// This compositor lets AnyBot see and use the screen (screencopy, virtual pointer and keyboard).
pub fn available() -> bool {
with(|_| Some(())).is_some()
}
/// A file only we can see, gone from the folder already (for shared memory and keymaps).
/// Its memory is really there before the compositor gets it: the compositor writes screen
/// pixels straight into it, and if the space ran out half-way the compositor would be the one
/// to crash (SIGBUS: a full /run/user once took a whole Hyprland desktop down this way).
/// So it lives in memory of its own (memfd), not in the runtime folder, and with no room the
/// screenshot just fails here.
fn scratch_file(len: u64) -> std::io::Result<std::fs::File> {
use std::os::fd::FromRawFd as _;
let fd = unsafe { libc::memfd_create(c"anybot-wl".as_ptr(), libc::MFD_CLOEXEC) };
let f = if fd >= 0 {
unsafe { std::fs::File::from_raw_fd(fd) }
} else {
let dir = std::env::var_os("XDG_RUNTIME_DIR").map(std::path::PathBuf::from).unwrap_or_else(std::env::temp_dir);
let path = dir.join(format!("anybot-wl-{}-{}", std::process::id(), crate::model::now_ms()));
let f = std::fs::OpenOptions::new().read(true).write(true).create_new(true).open(&path)?;
let _ = std::fs::remove_file(&path);
f
};
if len > 0 {
reserve(&f, len)?;
}
Ok(f)
}
/// Make every page of the file real now (not a promise the compositor finds broken later).
fn reserve(f: &std::fs::File, len: u64) -> std::io::Result<()> {
use std::os::fd::AsRawFd as _;
let r = unsafe { libc::posix_fallocate(f.as_raw_fd(), 0, len as libc::off_t) };
if r == 0 {
return Ok(());
}
if r != libc::EOPNOTSUPP && r != libc::EINVAL {
return Err(std::io::Error::from_raw_os_error(r));
}
// A file system without fallocate: write the zeros ourselves.
f.set_len(0)?;
let mut w = f;
let zeros = vec![0u8; 1 << 20];
let mut left = len;
while left > 0 {
let n = left.min(zeros.len() as u64) as usize;
w.write_all(&zeros[..n])?;
left -= n as u64;
}
Ok(())
}
/// The keymap text: one key per keysym we use; the first four are the modifiers.
pub fn keymap_text(keys: &[u32]) -> String {
let mut s = String::from("xkb_keymap {\nxkb_keycodes \"anybot\" {\n minimum = 8;\n");
s += &format!(" maximum = {};\n", 9 + keys.len().max(1));
for i in 0..keys.len() {
s += &format!(" <K{i}> = {};\n", i + 9);
}
s += "};\nxkb_types \"anybot\" { include \"complete\" };\nxkb_compat \"anybot\" { include \"complete\" };\nxkb_symbols \"anybot\" {\n";
for (i, k) in keys.iter().enumerate() {
s += &format!(" key <K{i}> {{ [ 0x{k:x} ] }};\n");
}
for (i, (_, _, name)) in MODS.iter().enumerate() {
s += &format!(" modifier_map {name} {{ <K{i}> }};\n");
}
s += "};\n};\n";
s
}
impl Wl {
fn time(&self) -> u32 {
self.start.elapsed().as_millis() as u32
}
fn upload_keymap(&mut self) -> std::io::Result<()> {
let mut text = keymap_text(&self.keys).into_bytes();
text.push(0);
let mut f = scratch_file(0)?;
f.write_all(&text)?;
self.kb.keymap(1, f.as_fd(), text.len() as u32);
let _ = self.conn.flush();
Ok(())
}
/// The evdev key that types `sym`, adding it to our keymap when it's new.
fn key_for(&mut self, sym: u32) -> u32 {
if let Some(i) = self.keys.iter().position(|k| *k == sym) {
return i as u32 + 1;
}
if self.keys.len() > 200 {
self.keys.truncate(MODS.len());
}
self.keys.push(sym);
let _ = self.upload_keymap();
let _ = self.queue.roundtrip(&mut self.st);
self.keys.len() as u32
}
fn key(&mut self, sym: u32, press: bool) {
let code = self.key_for(sym);
let t = self.time();
self.kb.key(t, code, press as u32);
if let Some((_, bit, _)) = MODS.iter().find(|m| m.0 == sym) {
self.mods = if press { self.mods | bit } else { self.mods & !bit };
self.kb.modifiers(self.mods, 0, 0, 0);
}
}
/// The whole layout: (left, top, width, height) in logical points.
fn layout(&self) -> (i32, i32, i32, i32) {
let rects: Vec<_> = self.st.outs.iter().map(|o| o.rect()).collect();
let x0 = rects.iter().map(|r| r.0).min().unwrap_or(0);
let y0 = rects.iter().map(|r| r.1).min().unwrap_or(0);
let x1 = rects.iter().map(|r| r.0 + r.2).max().unwrap_or(0);
let y1 = rects.iter().map(|r| r.1 + r.3).max().unwrap_or(0);
(x0, y0, (x1 - x0).max(1), (y1 - y0).max(1))
}
fn pixel_scale(&self) -> i32 {
self.st.outs.iter().map(|o| o.scale.max(1)).max().unwrap_or(1)
}
fn capture_output(&mut self, out: &wl_output::WlOutput) -> Option<image::RgbaImage> {
self.st.frame = Frame::default();
let frame = self.copy.capture_output(0, out, &self.qh, ());
// Every early exit passes through here, so the frame (and its buffer/pool) are always
// destroyed: failure paths used to leak compositor-side objects on every failed grab.
let r = self.grab(&frame);
frame.destroy();
r
}
fn grab(&mut self, frame: &frame_v1::ZwlrScreencopyFrameV1) -> Option<image::RgbaImage> {
let deadline = Instant::now() + Duration::from_secs(5);
while !(self.st.frame.failed || (self.st.frame.buffer.is_some() && (self.st.frame.buffer_done || self.copy_version < 3))) {
if Instant::now() > deadline {
return None;
}
self.queue.blocking_dispatch(&mut self.st).ok()?;
}
let (format, w, h, stride) = self.st.frame.buffer?;
let WEnum::Value(fmt) = format else { return None };
let size = (stride as u64) * (h as u64);
let file = scratch_file(size).ok()?;
let pool = self.shm.create_pool(file.as_fd(), size as i32, &self.qh, ());
let buffer = pool.create_buffer(0, w as i32, h as i32, stride as i32, fmt, &self.qh, ());
frame.copy(&buffer);
while !(self.st.frame.ready || self.st.frame.failed) {
if Instant::now() > deadline {
break;
}
self.queue.blocking_dispatch(&mut self.st).ok()?;
}
let ok = self.st.frame.ready;
let y_invert = self.st.frame.y_invert;
buffer.destroy();
pool.destroy();
if !ok {
return None;
}
let map = unsafe { memmap2::Mmap::map(&file) }.ok()?;
to_rgba(&map, fmt, w, h, stride, y_invert)
}
fn capture(&mut self) -> Option<image::RgbImage> {
let (x0, y0, lw, lh) = self.layout();
let s = self.pixel_scale();
let mut canvas = image::RgbaImage::new((lw * s) as u32, (lh * s) as u32);
let outs = self.st.outs.clone();
let mut any = false;
for o in outs {
let Some(out) = &o.out else { continue };
let Some(mut img) = self.capture_output(out) else { continue };
let (ox, oy, ow, oh) = o.rect();
let (tw, th) = ((ow * s) as u32, (oh * s) as u32);
if img.dimensions() != (tw, th) && tw > 0 && th > 0 {
img = image::imageops::resize(&img, tw, th, image::imageops::FilterType::Triangle);
}
image::imageops::replace(&mut canvas, &img, ((ox - x0) * s) as i64, ((oy - y0) * s) as i64);
any = true;
}
any.then(|| image::DynamicImage::ImageRgba8(canvas).to_rgb8())
}
}
/// Shared-memory pixels → RGBA. The 8-bit formats every compositor offers.
pub fn to_rgba(data: &[u8], fmt: wl_shm::Format, w: u32, h: u32, stride: u32, y_invert: bool) -> Option<image::RgbaImage> {
// In memory (little-endian): XRGB/ARGB = B G R X; XBGR/ABGR = R G B X.
let bgr = match fmt {
wl_shm::Format::Xrgb8888 | wl_shm::Format::Argb8888 => true,
wl_shm::Format::Xbgr8888 | wl_shm::Format::Abgr8888 => false,
_ => return None,
};
// Byte maths in usize: extreme resolutions overflow a u32 product (debug builds panic).
let mut out = Vec::with_capacity((w as usize) * (h as usize) * 4);
for row in 0..h {
let r = if y_invert { h - 1 - row } else { row };
let (a, b) = ((r as u64 * stride as u64) as usize, ((r as u64 * stride as u64) as usize) + (w as usize) * 4);
let line = data.get(a..b)?;
for px in line.chunks_exact(4) {
if bgr {
out.extend_from_slice(&[px[2], px[1], px[0], 255]);
} else {
out.extend_from_slice(&[px[0], px[1], px[2], 255]);
}
}
}
image::RgbaImage::from_raw(w, h, out)
}
// ---------- what desk.rs calls ----------
pub fn capture() -> Result<image::RgbImage, String> {
with(|wl| wl.capture()).ok_or_else(|| "讀不到螢幕畫面(Wayland 截圖失敗)".to_string())
}
/// The layout in logical points (where the mouse goes).
pub fn screen_points() -> (f64, f64) {
with(|wl| {
let (_, _, w, h) = wl.layout();
Some((w as f64, h as f64))
})
.unwrap_or((0.0, 0.0))
}
/// The screenshot's size in pixels.
pub fn screen_pixels() -> (u32, u32) {
with(|wl| {
let (_, _, w, h) = wl.layout();
let s = wl.pixel_scale();
Some(((w * s) as u32, (h * s) as u32))
})
.unwrap_or((0, 0))
}
pub fn motion(px: f64, py: f64) {
with(|wl| {
let (_, _, w, h) = wl.layout();
let t = wl.time();
let x = px.round().clamp(0.0, (w - 1) as f64) as u32;
let y = py.round().clamp(0.0, (h - 1) as f64) as u32;
wl.pointer.motion_absolute(t, x, y, w as u32, h as u32);
wl.pointer.frame();
Some(())
});
}
/// 1 left, 2 middle, 3 right (the X numbering desk.rs uses).
pub fn button(b: u8, press: bool) {
let code = match b {
3 => 0x111,
2 => 0x112,
_ => 0x110,
};
with(|wl| {
let t = wl.time();
wl.pointer.button(t, code, if press { wl_pointer::ButtonState::Pressed } else { wl_pointer::ButtonState::Released });
wl.pointer.frame();
Some(())
});
}
/// Wheel clicks: positive `up` scrolls up, positive `left` scrolls left (like X buttons 4 and 6).
pub fn wheel(up: i32, left: i32) {
with(|wl| {
for (n, axis) in [(up, wl_pointer::Axis::VerticalScroll), (left, wl_pointer::Axis::HorizontalScroll)] {
if n == 0 {
continue;
}
let t = wl.time();
wl.pointer.axis_source(wl_pointer::AxisSource::Wheel);
wl.pointer.axis_discrete(t, axis, -15.0 * n as f64, -n);
wl.pointer.frame();
}
Some(())
});
}
pub fn key(sym: u32, press: bool) {
with(|wl| {
wl.key(sym, press);
Some(())
});
}
/// Any text, Chinese too: each character's keysym goes on our keymap and is pressed.
pub fn type_text(text: &str) {
for c in text.chars() {
let sym = crate::x11::keysym_for_char(c);
with(|wl| {
wl.key(sym, true);
Some(())
});
std::thread::sleep(Duration::from_millis(8));
with(|wl| {
wl.key(sym, false);
Some(())
});
std::thread::sleep(Duration::from_millis(8));
}
}
/// The app in front (its Wayland app id, e.g. "firefox", "foot").
pub fn front_app() -> Option<(String, i32)> {
with(|wl| wl.st.tops.iter().find(|t| t.active && !t.app.is_empty()).map(|t| (t.app.clone(), 0)))
}
// ---------- idle time ----------
static IDLE: Mutex<Option<Option<Instant>>> = Mutex::new(None);
struct IdleState;
/// Seconds since the last input, from ext-idle-notify (a thread listening with a 1 s threshold).
/// None when the compositor doesn't offer it.
pub fn idle_secs() -> Option<f64> {
{
let g = IDLE.lock().unwrap_or_else(|e| e.into_inner());
if let Some(since) = *g {
return Some(since.map(|t| t.elapsed().as_secs_f64() + 1.0).unwrap_or(0.0));
}
}
static STARTED: std::sync::Once = std::sync::Once::new();
STARTED.call_once(|| {
std::thread::spawn(|| {
// Keep listening for the app's whole life: when the connection drops (compositor
// restart, read timeout) reconnect and listen again — the old loop quit forever
// after one break and idle_secs() went blind.
loop {
let Ok(conn) = Connection::connect_to_env() else { return };
let Ok((globals, mut queue)) = registry_queue_init::<IdleState>(&conn) else { return };
let qh = queue.handle();
let Ok(seat) = globals.bind::<wl_seat::WlSeat, _, _>(&qh, 1..=1, ()) else { return };
let Ok(notifier) = globals.bind::<ext_idle_notifier_v1::ExtIdleNotifierV1, _, _>(&qh, 1..=1, ()) else { return };
// Keep the notification object alive for as long as the loop runs: wayland-client
// destroys an object when its last handle drops, and dropping this return value
// here meant Idled events never arrived (idle_secs() was forever 0).
let _notification = notifier.get_idle_notification(1000, &seat, &qh, ());
*IDLE.lock().unwrap_or_else(|e| e.into_inner()) = Some(None);
let mut st = IdleState;
while queue.blocking_dispatch(&mut st).is_ok() {}
*IDLE.lock().unwrap_or_else(|e| e.into_inner()) = None;
std::thread::sleep(Duration::from_secs(2));
}
});
});
None
}
impl Dispatch<ext_idle_notification_v1::ExtIdleNotificationV1, ()> for IdleState {
fn event(_: &mut Self, _: &ext_idle_notification_v1::ExtIdleNotificationV1, event: ext_idle_notification_v1::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {
let v = match event {
ext_idle_notification_v1::Event::Idled => Some(Instant::now()),
_ => None,
};
*IDLE.lock().unwrap_or_else(|e| e.into_inner()) = Some(v);
}
}
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for IdleState {
fn event(_: &mut Self, _: &wl_registry::WlRegistry, _: wl_registry::Event, _: &GlobalListContents, _: &Connection, _: &QueueHandle<Self>) {}
}
delegate_noop!(IdleState: ignore wl_seat::WlSeat);
delegate_noop!(IdleState: ignore ext_idle_notifier_v1::ExtIdleNotifierV1);
// ---------- events ----------
impl Dispatch<wl_registry::WlRegistry, GlobalListContents> for State {
fn event(_: &mut Self, _: &wl_registry::WlRegistry, _: wl_registry::Event, _: &GlobalListContents, _: &Connection, _: &QueueHandle<Self>) {}
}
impl Dispatch<wl_output::WlOutput, u32> for State {
fn event(st: &mut Self, _: &wl_output::WlOutput, event: wl_output::Event, name: &u32, _: &Connection, _: &QueueHandle<Self>) {
let Some(o) = st.outs.iter_mut().find(|o| o.name == *name) else { return };
match event {
wl_output::Event::Geometry { x, y, .. } if !o.logical => {
o.x = x;
o.y = y;
}
wl_output::Event::Mode { flags: WEnum::Value(f), width, height, .. } if f.contains(wl_output::Mode::Current) => {
o.mode_w = width;
o.mode_h = height;
}
wl_output::Event::Scale { factor } => o.scale = factor.max(1),
_ => {}
}
}
}
impl Dispatch<zxdg_output_v1::ZxdgOutputV1, u32> for State {
fn event(st: &mut Self, _: &zxdg_output_v1::ZxdgOutputV1, event: zxdg_output_v1::Event, name: &u32, _: &Connection, _: &QueueHandle<Self>) {
let Some(o) = st.outs.iter_mut().find(|o| o.name == *name) else { return };
match event {
zxdg_output_v1::Event::LogicalPosition { x, y } => {
o.x = x;
o.y = y;
o.logical = true;
}
zxdg_output_v1::Event::LogicalSize { width, height } => {
o.lw = width;
o.lh = height;
o.logical = true;
}
_ => {}
}
}
}
impl Dispatch<frame_v1::ZwlrScreencopyFrameV1, ()> for State {
fn event(st: &mut Self, _: &frame_v1::ZwlrScreencopyFrameV1, event: frame_v1::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {
let f = &mut st.frame;
match event {
frame_v1::Event::Buffer { format, width, height, stride } => {
// Prefer a format we can read when several are offered.
let readable = matches!(format, WEnum::Value(wl_shm::Format::Xrgb8888 | wl_shm::Format::Argb8888 | wl_shm::Format::Xbgr8888 | wl_shm::Format::Abgr8888));
if f.buffer.is_none() || readable {
f.buffer = Some((format, width, height, stride));
}
}
frame_v1::Event::BufferDone => f.buffer_done = true,
frame_v1::Event::Flags { flags: WEnum::Value(fl) } => f.y_invert = fl.contains(frame_v1::Flags::YInvert),
frame_v1::Event::Ready { .. } => f.ready = true,
frame_v1::Event::Failed => f.failed = true,
_ => {}
}
}
}
impl Dispatch<top_manager::ZwlrForeignToplevelManagerV1, ()> for State {
fn event(st: &mut Self, _: &top_manager::ZwlrForeignToplevelManagerV1, event: top_manager::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {
if let top_manager::Event::Toplevel { toplevel } = event {
st.tops.push(Top { id: toplevel.id(), app: String::new(), active: false });
}
}
event_created_child!(State, top_manager::ZwlrForeignToplevelManagerV1, [
top_manager::EVT_TOPLEVEL_OPCODE => (top_handle::ZwlrForeignToplevelHandleV1, ()),
]);
}
impl Dispatch<top_handle::ZwlrForeignToplevelHandleV1, ()> for State {
fn event(st: &mut Self, h: &top_handle::ZwlrForeignToplevelHandleV1, event: top_handle::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {
let id = h.id();
match event {
top_handle::Event::Closed => st.tops.retain(|t| t.id != id),
top_handle::Event::AppId { app_id } => {
if let Some(t) = st.tops.iter_mut().find(|t| t.id == id) {
t.app = app_id;
}
}
top_handle::Event::State { state } => {
let active = state.chunks_exact(4).any(|c| u32::from_ne_bytes([c[0], c[1], c[2], c[3]]) == top_handle::State::Activated as u32);
if let Some(t) = st.tops.iter_mut().find(|t| t.id == id) {
t.active = active;
}
}
_ => {}
}
}
}
delegate_noop!(State: ignore wl_shm::WlShm);
delegate_noop!(State: ignore wl_shm_pool::WlShmPool);
delegate_noop!(State: ignore wl_buffer::WlBuffer);
delegate_noop!(State: ignore wl_seat::WlSeat);
delegate_noop!(State: ignore zxdg_output_manager_v1::ZxdgOutputManagerV1);
delegate_noop!(State: ignore zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1);
delegate_noop!(State: ignore zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1);
delegate_noop!(State: ignore zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1);
delegate_noop!(State: ignore zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1);
delegate_noop!(State: ignore zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keymap_and_pixels() {
let k = keymap_text(&[0xffe1, 0xffe3, 0xffe9, 0xffeb, 0x1004f60]);
assert!(k.contains("<K4> = 13;"));
assert!(k.contains("key <K4> { [ 0x1004f60 ] };"));
assert!(k.contains("modifier_map Control { <K1> };"));
// One BGRX pixel, upside down flag on a 1×2 image.
let px = to_rgba(&[1, 2, 3, 0, 4, 5, 6, 0], wl_shm::Format::Xrgb8888, 1, 2, 4, true).unwrap();
assert_eq!(px.get_pixel(0, 0).0, [6, 5, 4, 255]);
let px = to_rgba(&[1, 2, 3, 0], wl_shm::Format::Abgr8888, 1, 1, 4, false).unwrap();
assert_eq!(px.get_pixel(0, 0).0, [1, 2, 3, 255]);
}
/// The screenshot buffer is real memory before the compositor writes into it, and not a
/// file in the runtime folder (a full /run/user must not crash the desktop).
#[test]
fn screenshot_buffer_is_allocated_up_front() {
use std::os::fd::AsRawFd as _;
let len = 3 * 1024 * 1024 + 17;
let f = scratch_file(len).unwrap();
assert_eq!(f.metadata().unwrap().len(), len);
let mut st: libc::stat = unsafe { std::mem::zeroed() };
assert_eq!(unsafe { libc::fstat(f.as_raw_fd(), &mut st) }, 0);
assert!(st.st_blocks as u64 * 512 >= len, "pages not reserved: {} blocks", st.st_blocks);
let link = std::fs::read_link(format!("/proc/self/fd/{}", f.as_raw_fd())).unwrap();
assert!(link.to_string_lossy().starts_with("/memfd:"), "{link:?}");
}
}