AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
|
// 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")];
|
|
|
|
|
|
|
2026-11-05 07:48:22 +00:00
|
|
|
|
/// 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())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
|
fn connect() -> Option<Wl> {
|
|
|
|
|
|
let conn = Connection::connect_to_env().ok()?;
|
2026-11-05 07:48:22 +00:00
|
|
|
|
// 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()?;
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
|
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).
|
2026-11-05 12:06:46 +00:00
|
|
|
|
/// 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.
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
|
fn scratch_file(len: u64) -> std::io::Result<std::fs::File> {
|
2026-11-05 12:06:46 +00:00
|
|
|
|
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)?;
|
|
|
|
|
|
}
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
|
Ok(f)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 12:06:46 +00:00
|
|
|
|
/// 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(())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
|
/// 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, ());
|
2026-11-05 07:48:22 +00:00
|
|
|
|
// 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> {
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
|
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?;
|
2026-11-05 07:48:22 +00:00
|
|
|
|
let WEnum::Value(fmt) = format else { return None };
|
|
|
|
|
|
let size = (stride as u64) * (h as u64);
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
|
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,
|
|
|
|
|
|
};
|
2026-11-05 07:48:22 +00:00
|
|
|
|
// 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);
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
|
for row in 0..h {
|
|
|
|
|
|
let r = if y_invert { h - 1 - row } else { row };
|
2026-11-05 07:48:22 +00:00
|
|
|
|
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)?;
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
|
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(|| {
|
2026-11-05 07:48:22 +00:00
|
|
|
|
// 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));
|
|
|
|
|
|
}
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
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]);
|
|
|
|
|
|
}
|
2026-11-05 12:06:46 +00:00
|
|
|
|
|
|
|
|
|
|
/// 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:?}");
|
|
|
|
|
|
}
|
AnyBot: first commit of the working desktop app
Rust/egui rebuild of Grok Bot as of 2026-11-04: chat with streaming
providers (xAI, OpenAI, Claude, Gemini, OpenRouter, OpenCode Go, Ollama,
custom), tools and approvals, the bot's Docker computer, computer use on
macOS / Linux X11 / Wayland, AnyBot's browser with a full DevTools-based
toolset, the encrypted vault, pinned grid and sidebar sections, Markdown
with Mermaid diagrams, phone access through the relay, and the Linux
build and test scripts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uirha3oEtWeHh58zA7ErHa
2026-11-04 08:51:51 +00:00
|
|
|
|
}
|