AnyBot/src/wl.rs

670 lines
27 KiB
Rust
Raw Normal View History

// 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:?}");
}
}