mirror of
https://github.com/harivansh-afk/deskctl.git
synced 2026-04-17 14:01:22 +00:00
Rewrite X11 backend to drop xcap
Use x11rb directly for screenshot capture and window metadata so the Linux build no longer drags in Wayland build dependencies. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
parent
cc7490993a
commit
e392ba1055
11 changed files with 488 additions and 2128 deletions
1921
Cargo.lock
generated
1921
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -15,7 +15,6 @@ anyhow = "1"
|
||||||
dirs = "6"
|
dirs = "6"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
uuid = { version = "1", features = ["v4"] }
|
uuid = { version = "1", features = ["v4"] }
|
||||||
xcap = "0.8"
|
|
||||||
image = { version = "0.25", features = ["png"] }
|
image = { version = "0.25", features = ["png"] }
|
||||||
imageproc = "0.26"
|
imageproc = "0.26"
|
||||||
ab_glyph = "0.2"
|
ab_glyph = "0.2"
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,7 @@ pub fn annotate_screenshot(image: &mut RgbaImage, windows: &[WindowInfo]) {
|
||||||
if w > 0 && h > 0 {
|
if w > 0 && h > 0 {
|
||||||
draw_hollow_rect_mut(image, Rect::at(x, y).of_size(w, h), color);
|
draw_hollow_rect_mut(image, Rect::at(x, y).of_size(w, h), color);
|
||||||
if w > 2 && h > 2 {
|
if w > 2 && h > 2 {
|
||||||
draw_hollow_rect_mut(
|
draw_hollow_rect_mut(image, Rect::at(x + 1, y + 1).of_size(w - 2, h - 2), color);
|
||||||
image,
|
|
||||||
Rect::at(x + 1, y + 1).of_size(w - 2, h - 2),
|
|
||||||
color,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -67,6 +63,14 @@ pub fn annotate_screenshot(image: &mut RgbaImage, windows: &[WindowInfo]) {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Label text
|
// Label text
|
||||||
draw_text_mut(image, LABEL_FG, label_x + 3, label_y + 2, scale, &font, &label);
|
draw_text_mut(
|
||||||
|
image,
|
||||||
|
LABEL_FG,
|
||||||
|
label_x + 3,
|
||||||
|
label_y + 2,
|
||||||
|
scale,
|
||||||
|
&font,
|
||||||
|
&label,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
pub mod annotate;
|
pub mod annotate;
|
||||||
pub mod x11;
|
pub mod x11;
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use crate::core::types::Snapshot;
|
use crate::core::types::Snapshot;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub trait DesktopBackend: Send {
|
pub trait DesktopBackend: Send {
|
||||||
|
|
|
||||||
|
|
@ -1,86 +1,240 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use enigo::{
|
use enigo::{Axis, Button, Coordinate, Direction, Enigo, Key, Keyboard, Mouse, Settings};
|
||||||
Axis, Button, Coordinate, Direction, Enigo, Key, Keyboard, Mouse, Settings,
|
use image::RgbaImage;
|
||||||
};
|
|
||||||
use x11rb::connection::Connection;
|
use x11rb::connection::Connection;
|
||||||
use x11rb::protocol::xproto::{
|
use x11rb::protocol::xproto::{
|
||||||
ClientMessageEvent, ConfigureWindowAux, ConnectionExt as XprotoConnectionExt,
|
Atom, AtomEnum, ClientMessageData, ClientMessageEvent, ConfigureWindowAux,
|
||||||
EventMask,
|
ConnectionExt as XprotoConnectionExt, EventMask, GetPropertyReply, ImageFormat, ImageOrder,
|
||||||
|
Window,
|
||||||
};
|
};
|
||||||
use x11rb::rust_connection::RustConnection;
|
use x11rb::rust_connection::RustConnection;
|
||||||
|
|
||||||
use super::annotate::annotate_screenshot;
|
use super::annotate::annotate_screenshot;
|
||||||
use crate::core::types::{Snapshot, WindowInfo};
|
use crate::core::types::{Snapshot, WindowInfo};
|
||||||
|
|
||||||
|
struct Atoms {
|
||||||
|
client_list_stacking: Atom,
|
||||||
|
active_window: Atom,
|
||||||
|
net_wm_name: Atom,
|
||||||
|
utf8_string: Atom,
|
||||||
|
wm_name: Atom,
|
||||||
|
wm_class: Atom,
|
||||||
|
net_wm_state: Atom,
|
||||||
|
net_wm_state_hidden: Atom,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct X11Backend {
|
pub struct X11Backend {
|
||||||
enigo: Enigo,
|
enigo: Enigo,
|
||||||
conn: RustConnection,
|
conn: RustConnection,
|
||||||
root: u32,
|
root: Window,
|
||||||
|
atoms: Atoms,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl X11Backend {
|
impl X11Backend {
|
||||||
pub fn new() -> Result<Self> {
|
pub fn new() -> Result<Self> {
|
||||||
let enigo = Enigo::new(&Settings::default())
|
let enigo = Enigo::new(&Settings::default())
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to initialize enigo: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("Failed to initialize enigo: {e}"))?;
|
||||||
let (conn, screen_num) = x11rb::connect(None)
|
let (conn, screen_num) = x11rb::connect(None).context("Failed to connect to X11 server")?;
|
||||||
.context("Failed to connect to X11 server")?;
|
|
||||||
let root = conn.setup().roots[screen_num].root;
|
let root = conn.setup().roots[screen_num].root;
|
||||||
Ok(Self { enigo, conn, root })
|
let atoms = Atoms::new(&conn)?;
|
||||||
|
Ok(Self {
|
||||||
|
enigo,
|
||||||
|
conn,
|
||||||
|
root,
|
||||||
|
atoms,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl super::DesktopBackend for X11Backend {
|
fn stacked_windows(&self) -> Result<Vec<Window>> {
|
||||||
fn snapshot(&mut self, annotate: bool) -> Result<Snapshot> {
|
let mut windows = self
|
||||||
// Get z-ordered window list via xcap (topmost first internally)
|
.get_property_u32(
|
||||||
let windows = xcap::Window::all().context("Failed to enumerate windows")?;
|
self.root,
|
||||||
|
self.atoms.client_list_stacking,
|
||||||
|
AtomEnum::WINDOW.into(),
|
||||||
|
1024,
|
||||||
|
)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|id| id as Window)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Get primary monitor for screenshot
|
if windows.is_empty() {
|
||||||
let monitors = xcap::Monitor::all().context("Failed to enumerate monitors")?;
|
windows = self
|
||||||
let monitor = monitors.into_iter().next().context("No monitor found")?;
|
.conn
|
||||||
|
.query_tree(self.root)?
|
||||||
|
.reply()
|
||||||
|
.context("Failed to query root window tree")?
|
||||||
|
.children;
|
||||||
|
}
|
||||||
|
|
||||||
let mut image = monitor
|
// EWMH exposes bottom-to-top stacking order. Reverse it so @w1 is the topmost window.
|
||||||
.capture_image()
|
windows.reverse();
|
||||||
.context("Failed to capture screenshot")?;
|
Ok(windows)
|
||||||
|
}
|
||||||
|
|
||||||
// Build window info list
|
fn collect_window_infos(&self) -> Result<Vec<WindowInfo>> {
|
||||||
|
let active_window = self.active_window()?;
|
||||||
let mut window_infos = Vec::new();
|
let mut window_infos = Vec::new();
|
||||||
let mut ref_counter = 1usize;
|
let mut ref_counter = 1usize;
|
||||||
|
|
||||||
for win in &windows {
|
for window in self.stacked_windows()? {
|
||||||
// Each xcap method returns XCapResult<T> - skip windows where metadata fails
|
let title = self.window_title(window).unwrap_or_default();
|
||||||
let title = win.title().unwrap_or_default();
|
let app_name = self.window_app_name(window).unwrap_or_default();
|
||||||
let app_name = win.app_name().unwrap_or_default();
|
|
||||||
|
|
||||||
// Skip windows with empty titles and app names (desktop, panels, etc.)
|
|
||||||
if title.is_empty() && app_name.is_empty() {
|
if title.is_empty() && app_name.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let xcb_id = win.id().unwrap_or(0);
|
let (x, y, width, height) = match self.window_geometry(window) {
|
||||||
let x = win.x().unwrap_or(0);
|
Ok(geometry) => geometry,
|
||||||
let y = win.y().unwrap_or(0);
|
Err(_) => continue,
|
||||||
let width = win.width().unwrap_or(0);
|
};
|
||||||
let height = win.height().unwrap_or(0);
|
|
||||||
let focused = win.is_focused().unwrap_or(false);
|
|
||||||
let minimized = win.is_minimized().unwrap_or(false);
|
|
||||||
|
|
||||||
let ref_id = format!("w{ref_counter}");
|
|
||||||
ref_counter += 1;
|
|
||||||
|
|
||||||
|
let minimized = self.window_is_minimized(window).unwrap_or(false);
|
||||||
window_infos.push(WindowInfo {
|
window_infos.push(WindowInfo {
|
||||||
ref_id,
|
ref_id: format!("w{ref_counter}"),
|
||||||
xcb_id,
|
xcb_id: window,
|
||||||
title,
|
title,
|
||||||
app_name,
|
app_name,
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
focused,
|
focused: active_window == Some(window),
|
||||||
minimized,
|
minimized,
|
||||||
});
|
});
|
||||||
|
ref_counter += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(window_infos)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capture_root_image(&self) -> Result<RgbaImage> {
|
||||||
|
let (width, height) = self.root_geometry()?;
|
||||||
|
let reply = self
|
||||||
|
.conn
|
||||||
|
.get_image(
|
||||||
|
ImageFormat::Z_PIXMAP,
|
||||||
|
self.root,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
width as u16,
|
||||||
|
height as u16,
|
||||||
|
u32::MAX,
|
||||||
|
)?
|
||||||
|
.reply()
|
||||||
|
.context("Failed to capture root window image")?;
|
||||||
|
|
||||||
|
rgba_from_image_reply(self.conn.setup(), width, height, &reply)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn root_geometry(&self) -> Result<(u32, u32)> {
|
||||||
|
let geometry = self
|
||||||
|
.conn
|
||||||
|
.get_geometry(self.root)?
|
||||||
|
.reply()
|
||||||
|
.context("Failed to get root geometry")?;
|
||||||
|
Ok((geometry.width.into(), geometry.height.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_geometry(&self, window: Window) -> Result<(i32, i32, u32, u32)> {
|
||||||
|
let geometry = self
|
||||||
|
.conn
|
||||||
|
.get_geometry(window)?
|
||||||
|
.reply()
|
||||||
|
.context("Failed to get window geometry")?;
|
||||||
|
let translated = self
|
||||||
|
.conn
|
||||||
|
.translate_coordinates(window, self.root, 0, 0)?
|
||||||
|
.reply()
|
||||||
|
.context("Failed to translate window coordinates to root")?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
i32::from(translated.dst_x),
|
||||||
|
i32::from(translated.dst_y),
|
||||||
|
geometry.width.into(),
|
||||||
|
geometry.height.into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn active_window(&self) -> Result<Option<Window>> {
|
||||||
|
Ok(self
|
||||||
|
.get_property_u32(
|
||||||
|
self.root,
|
||||||
|
self.atoms.active_window,
|
||||||
|
AtomEnum::WINDOW.into(),
|
||||||
|
1,
|
||||||
|
)?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.map(|id| id as Window))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_title(&self, window: Window) -> Result<String> {
|
||||||
|
let title =
|
||||||
|
self.read_text_property(window, self.atoms.net_wm_name, self.atoms.utf8_string)?;
|
||||||
|
if !title.is_empty() {
|
||||||
|
return Ok(title);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.read_text_property(window, self.atoms.wm_name, AtomEnum::ANY.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_app_name(&self, window: Window) -> Result<String> {
|
||||||
|
let wm_class =
|
||||||
|
self.read_text_property(window, self.atoms.wm_class, AtomEnum::STRING.into())?;
|
||||||
|
let mut parts = wm_class.split('\0').filter(|part| !part.is_empty());
|
||||||
|
Ok(parts
|
||||||
|
.nth(1)
|
||||||
|
.or_else(|| parts.next())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_is_minimized(&self, window: Window) -> Result<bool> {
|
||||||
|
let states =
|
||||||
|
self.get_property_u32(window, self.atoms.net_wm_state, AtomEnum::ATOM.into(), 32)?;
|
||||||
|
Ok(states.contains(&self.atoms.net_wm_state_hidden))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_text_property(&self, window: Window, property: Atom, type_: Atom) -> Result<String> {
|
||||||
|
let reply = self.get_property(window, property, type_, 1024)?;
|
||||||
|
Ok(String::from_utf8_lossy(&reply.value)
|
||||||
|
.trim_end_matches('\0')
|
||||||
|
.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_property_u32(
|
||||||
|
&self,
|
||||||
|
window: Window,
|
||||||
|
property: Atom,
|
||||||
|
type_: Atom,
|
||||||
|
long_length: u32,
|
||||||
|
) -> Result<Vec<u32>> {
|
||||||
|
let reply = self.get_property(window, property, type_, long_length)?;
|
||||||
|
Ok(reply
|
||||||
|
.value32()
|
||||||
|
.map(|iter| iter.collect::<Vec<_>>())
|
||||||
|
.unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_property(
|
||||||
|
&self,
|
||||||
|
window: Window,
|
||||||
|
property: Atom,
|
||||||
|
type_: Atom,
|
||||||
|
long_length: u32,
|
||||||
|
) -> Result<GetPropertyReply> {
|
||||||
|
self.conn
|
||||||
|
.get_property(false, window, property, type_, 0, long_length)?
|
||||||
|
.reply()
|
||||||
|
.with_context(|| format!("Failed to read property {property} from window {window}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::DesktopBackend for X11Backend {
|
||||||
|
fn snapshot(&mut self, annotate: bool) -> Result<Snapshot> {
|
||||||
|
let window_infos = self.collect_window_infos()?;
|
||||||
|
let mut image = self.capture_root_image()?;
|
||||||
|
|
||||||
// Annotate if requested - draw bounding boxes and @wN labels
|
// Annotate if requested - draw bounding boxes and @wN labels
|
||||||
if annotate {
|
if annotate {
|
||||||
annotate_screenshot(&mut image, &window_infos);
|
annotate_screenshot(&mut image, &window_infos);
|
||||||
|
|
@ -117,7 +271,7 @@ impl super::DesktopBackend for X11Backend {
|
||||||
sequence: 0,
|
sequence: 0,
|
||||||
window: xcb_id,
|
window: xcb_id,
|
||||||
type_: net_active,
|
type_: net_active,
|
||||||
data: x11rb::protocol::xproto::ClientMessageData::from([
|
data: ClientMessageData::from([
|
||||||
2u32, 0, 0, 0, 0, // source=2 (pager), timestamp=0, currently_active=0
|
2u32, 0, 0, 0, 0, // source=2 (pager), timestamp=0, currently_active=0
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|
@ -128,21 +282,27 @@ impl super::DesktopBackend for X11Backend {
|
||||||
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
||||||
event,
|
event,
|
||||||
)?;
|
)?;
|
||||||
self.conn.flush().context("Failed to flush X11 connection")?;
|
self.conn
|
||||||
|
.flush()
|
||||||
|
.context("Failed to flush X11 connection")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_window(&mut self, xcb_id: u32, x: i32, y: i32) -> Result<()> {
|
fn move_window(&mut self, xcb_id: u32, x: i32, y: i32) -> Result<()> {
|
||||||
self.conn
|
self.conn
|
||||||
.configure_window(xcb_id, &ConfigureWindowAux::new().x(x).y(y))?;
|
.configure_window(xcb_id, &ConfigureWindowAux::new().x(x).y(y))?;
|
||||||
self.conn.flush().context("Failed to flush X11 connection")?;
|
self.conn
|
||||||
|
.flush()
|
||||||
|
.context("Failed to flush X11 connection")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resize_window(&mut self, xcb_id: u32, w: u32, h: u32) -> Result<()> {
|
fn resize_window(&mut self, xcb_id: u32, w: u32, h: u32) -> Result<()> {
|
||||||
self.conn
|
self.conn
|
||||||
.configure_window(xcb_id, &ConfigureWindowAux::new().width(w).height(h))?;
|
.configure_window(xcb_id, &ConfigureWindowAux::new().width(w).height(h))?;
|
||||||
self.conn.flush().context("Failed to flush X11 connection")?;
|
self.conn
|
||||||
|
.flush()
|
||||||
|
.context("Failed to flush X11 connection")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,7 +321,7 @@ impl super::DesktopBackend for X11Backend {
|
||||||
sequence: 0,
|
sequence: 0,
|
||||||
window: xcb_id,
|
window: xcb_id,
|
||||||
type_: net_close,
|
type_: net_close,
|
||||||
data: x11rb::protocol::xproto::ClientMessageData::from([
|
data: ClientMessageData::from([
|
||||||
0u32, 2, 0, 0, 0, // timestamp=0, source=2 (pager)
|
0u32, 2, 0, 0, 0, // timestamp=0, source=2 (pager)
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|
@ -172,7 +332,9 @@ impl super::DesktopBackend for X11Backend {
|
||||||
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
EventMask::SUBSTRUCTURE_REDIRECT | EventMask::SUBSTRUCTURE_NOTIFY,
|
||||||
event,
|
event,
|
||||||
)?;
|
)?;
|
||||||
self.conn.flush().context("Failed to flush X11 connection")?;
|
self.conn
|
||||||
|
.flush()
|
||||||
|
.context("Failed to flush X11 connection")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -289,11 +451,7 @@ impl super::DesktopBackend for X11Backend {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn screen_size(&self) -> Result<(u32, u32)> {
|
fn screen_size(&self) -> Result<(u32, u32)> {
|
||||||
let monitors = xcap::Monitor::all().context("Failed to enumerate monitors")?;
|
self.root_geometry()
|
||||||
let monitor = monitors.into_iter().next().context("No monitor found")?;
|
|
||||||
let w = monitor.width().context("Failed to get monitor width")?;
|
|
||||||
let h = monitor.height().context("Failed to get monitor height")?;
|
|
||||||
Ok((w, h))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mouse_position(&self) -> Result<(i32, i32)> {
|
fn mouse_position(&self) -> Result<(i32, i32)> {
|
||||||
|
|
@ -306,37 +464,10 @@ impl super::DesktopBackend for X11Backend {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn screenshot(&mut self, path: &str, annotate: bool) -> Result<String> {
|
fn screenshot(&mut self, path: &str, annotate: bool) -> Result<String> {
|
||||||
let monitors = xcap::Monitor::all().context("Failed to enumerate monitors")?;
|
let mut image = self.capture_root_image()?;
|
||||||
let monitor = monitors.into_iter().next().context("No monitor found")?;
|
|
||||||
|
|
||||||
let mut image = monitor
|
|
||||||
.capture_image()
|
|
||||||
.context("Failed to capture screenshot")?;
|
|
||||||
|
|
||||||
if annotate {
|
if annotate {
|
||||||
let windows = xcap::Window::all().unwrap_or_default();
|
let window_infos = self.collect_window_infos()?;
|
||||||
let mut window_infos = Vec::new();
|
|
||||||
let mut ref_counter = 1usize;
|
|
||||||
for win in &windows {
|
|
||||||
let title = win.title().unwrap_or_default();
|
|
||||||
let app_name = win.app_name().unwrap_or_default();
|
|
||||||
if title.is_empty() && app_name.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
window_infos.push(crate::core::types::WindowInfo {
|
|
||||||
ref_id: format!("w{ref_counter}"),
|
|
||||||
xcb_id: win.id().unwrap_or(0),
|
|
||||||
title,
|
|
||||||
app_name,
|
|
||||||
x: win.x().unwrap_or(0),
|
|
||||||
y: win.y().unwrap_or(0),
|
|
||||||
width: win.width().unwrap_or(0),
|
|
||||||
height: win.height().unwrap_or(0),
|
|
||||||
focused: win.is_focused().unwrap_or(false),
|
|
||||||
minimized: win.is_minimized().unwrap_or(false),
|
|
||||||
});
|
|
||||||
ref_counter += 1;
|
|
||||||
}
|
|
||||||
annotate_screenshot(&mut image, &window_infos);
|
annotate_screenshot(&mut image, &window_infos);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -404,3 +535,121 @@ fn parse_key(name: &str) -> Result<Key> {
|
||||||
other => anyhow::bail!("Unknown key: {other}"),
|
other => anyhow::bail!("Unknown key: {other}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Atoms {
|
||||||
|
fn new(conn: &RustConnection) -> Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
client_list_stacking: intern_atom(conn, b"_NET_CLIENT_LIST_STACKING")?,
|
||||||
|
active_window: intern_atom(conn, b"_NET_ACTIVE_WINDOW")?,
|
||||||
|
net_wm_name: intern_atom(conn, b"_NET_WM_NAME")?,
|
||||||
|
utf8_string: intern_atom(conn, b"UTF8_STRING")?,
|
||||||
|
wm_name: intern_atom(conn, b"WM_NAME")?,
|
||||||
|
wm_class: intern_atom(conn, b"WM_CLASS")?,
|
||||||
|
net_wm_state: intern_atom(conn, b"_NET_WM_STATE")?,
|
||||||
|
net_wm_state_hidden: intern_atom(conn, b"_NET_WM_STATE_HIDDEN")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn intern_atom(conn: &RustConnection, name: &[u8]) -> Result<Atom> {
|
||||||
|
conn.intern_atom(false, name)?
|
||||||
|
.reply()
|
||||||
|
.with_context(|| format!("Failed to intern atom {}", String::from_utf8_lossy(name)))
|
||||||
|
.map(|reply| reply.atom)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rgba_from_image_reply(
|
||||||
|
setup: &x11rb::protocol::xproto::Setup,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
reply: &x11rb::protocol::xproto::GetImageReply,
|
||||||
|
) -> Result<RgbaImage> {
|
||||||
|
let pixmap_format = setup
|
||||||
|
.pixmap_formats
|
||||||
|
.iter()
|
||||||
|
.find(|format| format.depth == reply.depth)
|
||||||
|
.context("Failed to find pixmap format for captured image depth")?;
|
||||||
|
let bits_per_pixel = u32::from(pixmap_format.bits_per_pixel);
|
||||||
|
let bit_order = setup.bitmap_format_bit_order;
|
||||||
|
let bytes = reply.data.as_slice();
|
||||||
|
|
||||||
|
let get_pixel_rgba = match reply.depth {
|
||||||
|
8 => pixel8_rgba,
|
||||||
|
16 => pixel16_rgba,
|
||||||
|
24 | 32 => pixel24_32_rgba,
|
||||||
|
depth => anyhow::bail!("Unsupported X11 image depth: {depth}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut rgba = vec![0u8; (width * height * 4) as usize];
|
||||||
|
for y in 0..height {
|
||||||
|
for x in 0..width {
|
||||||
|
let index = ((y * width + x) * 4) as usize;
|
||||||
|
let (r, g, b, a) = get_pixel_rgba(bytes, x, y, width, bits_per_pixel, bit_order);
|
||||||
|
rgba[index] = r;
|
||||||
|
rgba[index + 1] = g;
|
||||||
|
rgba[index + 2] = b;
|
||||||
|
rgba[index + 3] = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RgbaImage::from_raw(width, height, rgba)
|
||||||
|
.context("Failed to convert captured X11 image into RGBA buffer")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pixel8_rgba(
|
||||||
|
bytes: &[u8],
|
||||||
|
x: u32,
|
||||||
|
y: u32,
|
||||||
|
width: u32,
|
||||||
|
bits_per_pixel: u32,
|
||||||
|
bit_order: ImageOrder,
|
||||||
|
) -> (u8, u8, u8, u8) {
|
||||||
|
let index = ((y * width + x) * bits_per_pixel / 8) as usize;
|
||||||
|
let pixel = if bit_order == ImageOrder::LSB_FIRST {
|
||||||
|
bytes[index]
|
||||||
|
} else {
|
||||||
|
bytes[index] & (7 << 4) | (bytes[index] >> 4)
|
||||||
|
};
|
||||||
|
|
||||||
|
let r = (pixel >> 6) as f32 / 3.0 * 255.0;
|
||||||
|
let g = ((pixel >> 2) & 7) as f32 / 7.0 * 255.0;
|
||||||
|
let b = (pixel & 3) as f32 / 3.0 * 255.0;
|
||||||
|
(r as u8, g as u8, b as u8, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pixel16_rgba(
|
||||||
|
bytes: &[u8],
|
||||||
|
x: u32,
|
||||||
|
y: u32,
|
||||||
|
width: u32,
|
||||||
|
bits_per_pixel: u32,
|
||||||
|
bit_order: ImageOrder,
|
||||||
|
) -> (u8, u8, u8, u8) {
|
||||||
|
let index = ((y * width + x) * bits_per_pixel / 8) as usize;
|
||||||
|
let pixel = if bit_order == ImageOrder::LSB_FIRST {
|
||||||
|
u16::from(bytes[index]) | (u16::from(bytes[index + 1]) << 8)
|
||||||
|
} else {
|
||||||
|
(u16::from(bytes[index]) << 8) | u16::from(bytes[index + 1])
|
||||||
|
};
|
||||||
|
|
||||||
|
let r = (pixel >> 11) as f32 / 31.0 * 255.0;
|
||||||
|
let g = ((pixel >> 5) & 63) as f32 / 63.0 * 255.0;
|
||||||
|
let b = (pixel & 31) as f32 / 31.0 * 255.0;
|
||||||
|
(r as u8, g as u8, b as u8, 255)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pixel24_32_rgba(
|
||||||
|
bytes: &[u8],
|
||||||
|
x: u32,
|
||||||
|
y: u32,
|
||||||
|
width: u32,
|
||||||
|
bits_per_pixel: u32,
|
||||||
|
bit_order: ImageOrder,
|
||||||
|
) -> (u8, u8, u8, u8) {
|
||||||
|
let index = ((y * width + x) * bits_per_pixel / 8) as usize;
|
||||||
|
if bit_order == ImageOrder::LSB_FIRST {
|
||||||
|
(bytes[index + 2], bytes[index + 1], bytes[index], 255)
|
||||||
|
} else {
|
||||||
|
(bytes[index], bytes[index + 1], bytes[index + 2], 255)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,12 +39,10 @@ fn try_connect(opts: &GlobalOpts) -> Option<UnixStream> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_daemon(opts: &GlobalOpts) -> Result<()> {
|
fn spawn_daemon(opts: &GlobalOpts) -> Result<()> {
|
||||||
let exe = std::env::current_exe()
|
let exe = std::env::current_exe().context("Failed to determine executable path")?;
|
||||||
.context("Failed to determine executable path")?;
|
|
||||||
|
|
||||||
let sock_dir = socket_dir();
|
let sock_dir = socket_dir();
|
||||||
std::fs::create_dir_all(&sock_dir)
|
std::fs::create_dir_all(&sock_dir).context("Failed to create socket directory")?;
|
||||||
.context("Failed to create socket directory")?;
|
|
||||||
|
|
||||||
let mut cmd = Command::new(exe);
|
let mut cmd = Command::new(exe);
|
||||||
cmd.env("DESKCTL_DAEMON", "1")
|
cmd.env("DESKCTL_DAEMON", "1")
|
||||||
|
|
@ -109,8 +107,8 @@ pub fn send_command(opts: &GlobalOpts, request: &Request) -> Result<Response> {
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
reader.read_line(&mut line)?;
|
reader.read_line(&mut line)?;
|
||||||
|
|
||||||
let response: Response = serde_json::from_str(line.trim())
|
let response: Response =
|
||||||
.context("Failed to parse daemon response")?;
|
serde_json::from_str(line.trim()).context("Failed to parse daemon response")?;
|
||||||
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
mod connection;
|
mod connection;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
use clap::{Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
use crate::core::protocol::{Request, Response};
|
use crate::core::protocol::{Request, Response};
|
||||||
|
|
||||||
|
|
@ -196,84 +196,57 @@ fn build_request(cmd: &Command) -> Result<Request> {
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
let req = match cmd {
|
let req = match cmd {
|
||||||
Command::Snapshot { annotate } => {
|
Command::Snapshot { annotate } => {
|
||||||
Request::new("snapshot")
|
Request::new("snapshot").with_extra("annotate", json!(annotate))
|
||||||
.with_extra("annotate", json!(annotate))
|
|
||||||
}
|
}
|
||||||
Command::Click { selector } => {
|
Command::Click { selector } => {
|
||||||
Request::new("click")
|
Request::new("click").with_extra("selector", json!(selector))
|
||||||
.with_extra("selector", json!(selector))
|
|
||||||
}
|
}
|
||||||
Command::Dblclick { selector } => {
|
Command::Dblclick { selector } => {
|
||||||
Request::new("dblclick")
|
Request::new("dblclick").with_extra("selector", json!(selector))
|
||||||
.with_extra("selector", json!(selector))
|
|
||||||
}
|
|
||||||
Command::Type { text } => {
|
|
||||||
Request::new("type")
|
|
||||||
.with_extra("text", json!(text))
|
|
||||||
}
|
|
||||||
Command::Press { key } => {
|
|
||||||
Request::new("press")
|
|
||||||
.with_extra("key", json!(key))
|
|
||||||
}
|
|
||||||
Command::Hotkey { keys } => {
|
|
||||||
Request::new("hotkey")
|
|
||||||
.with_extra("keys", json!(keys))
|
|
||||||
}
|
}
|
||||||
|
Command::Type { text } => Request::new("type").with_extra("text", json!(text)),
|
||||||
|
Command::Press { key } => Request::new("press").with_extra("key", json!(key)),
|
||||||
|
Command::Hotkey { keys } => Request::new("hotkey").with_extra("keys", json!(keys)),
|
||||||
Command::Mouse(sub) => match sub {
|
Command::Mouse(sub) => match sub {
|
||||||
MouseCmd::Move { x, y } => {
|
MouseCmd::Move { x, y } => Request::new("mouse-move")
|
||||||
Request::new("mouse-move")
|
|
||||||
.with_extra("x", json!(x))
|
.with_extra("x", json!(x))
|
||||||
.with_extra("y", json!(y))
|
.with_extra("y", json!(y)),
|
||||||
}
|
MouseCmd::Scroll { amount, axis } => Request::new("mouse-scroll")
|
||||||
MouseCmd::Scroll { amount, axis } => {
|
|
||||||
Request::new("mouse-scroll")
|
|
||||||
.with_extra("amount", json!(amount))
|
.with_extra("amount", json!(amount))
|
||||||
.with_extra("axis", json!(axis))
|
.with_extra("axis", json!(axis)),
|
||||||
}
|
MouseCmd::Drag { x1, y1, x2, y2 } => Request::new("mouse-drag")
|
||||||
MouseCmd::Drag { x1, y1, x2, y2 } => {
|
|
||||||
Request::new("mouse-drag")
|
|
||||||
.with_extra("x1", json!(x1))
|
.with_extra("x1", json!(x1))
|
||||||
.with_extra("y1", json!(y1))
|
.with_extra("y1", json!(y1))
|
||||||
.with_extra("x2", json!(x2))
|
.with_extra("x2", json!(x2))
|
||||||
.with_extra("y2", json!(y2))
|
.with_extra("y2", json!(y2)),
|
||||||
}
|
|
||||||
},
|
},
|
||||||
Command::Focus { selector } => {
|
Command::Focus { selector } => {
|
||||||
Request::new("focus")
|
Request::new("focus").with_extra("selector", json!(selector))
|
||||||
.with_extra("selector", json!(selector))
|
|
||||||
}
|
}
|
||||||
Command::Close { selector } => {
|
Command::Close { selector } => {
|
||||||
Request::new("close")
|
Request::new("close").with_extra("selector", json!(selector))
|
||||||
.with_extra("selector", json!(selector))
|
|
||||||
}
|
}
|
||||||
Command::MoveWindow { selector, x, y } => {
|
Command::MoveWindow { selector, x, y } => Request::new("move-window")
|
||||||
Request::new("move-window")
|
|
||||||
.with_extra("selector", json!(selector))
|
.with_extra("selector", json!(selector))
|
||||||
.with_extra("x", json!(x))
|
.with_extra("x", json!(x))
|
||||||
.with_extra("y", json!(y))
|
.with_extra("y", json!(y)),
|
||||||
}
|
Command::ResizeWindow { selector, w, h } => Request::new("resize-window")
|
||||||
Command::ResizeWindow { selector, w, h } => {
|
|
||||||
Request::new("resize-window")
|
|
||||||
.with_extra("selector", json!(selector))
|
.with_extra("selector", json!(selector))
|
||||||
.with_extra("w", json!(w))
|
.with_extra("w", json!(w))
|
||||||
.with_extra("h", json!(h))
|
.with_extra("h", json!(h)),
|
||||||
}
|
|
||||||
Command::ListWindows => Request::new("list-windows"),
|
Command::ListWindows => Request::new("list-windows"),
|
||||||
Command::GetScreenSize => Request::new("get-screen-size"),
|
Command::GetScreenSize => Request::new("get-screen-size"),
|
||||||
Command::GetMousePosition => Request::new("get-mouse-position"),
|
Command::GetMousePosition => Request::new("get-mouse-position"),
|
||||||
Command::Screenshot { path, annotate } => {
|
Command::Screenshot { path, annotate } => {
|
||||||
let mut req = Request::new("screenshot")
|
let mut req = Request::new("screenshot").with_extra("annotate", json!(annotate));
|
||||||
.with_extra("annotate", json!(annotate));
|
|
||||||
if let Some(p) = path {
|
if let Some(p) = path {
|
||||||
req = req.with_extra("path", json!(p.to_string_lossy()));
|
req = req.with_extra("path", json!(p.to_string_lossy()));
|
||||||
}
|
}
|
||||||
req
|
req
|
||||||
}
|
}
|
||||||
Command::Launch { command, args } => {
|
Command::Launch { command, args } => Request::new("launch")
|
||||||
Request::new("launch")
|
|
||||||
.with_extra("command", json!(command))
|
.with_extra("command", json!(command))
|
||||||
.with_extra("args", json!(args))
|
.with_extra("args", json!(args)),
|
||||||
}
|
|
||||||
Command::Daemon(_) => unreachable!(),
|
Command::Daemon(_) => unreachable!(),
|
||||||
};
|
};
|
||||||
Ok(req)
|
Ok(req)
|
||||||
|
|
@ -298,18 +271,30 @@ fn print_response(cmd: &Command, response: &Response) -> Result<()> {
|
||||||
let ref_id = w.get("ref_id").and_then(|v| v.as_str()).unwrap_or("?");
|
let ref_id = w.get("ref_id").and_then(|v| v.as_str()).unwrap_or("?");
|
||||||
let title = w.get("title").and_then(|v| v.as_str()).unwrap_or("");
|
let title = w.get("title").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
let focused = w.get("focused").and_then(|v| v.as_bool()).unwrap_or(false);
|
let focused = w.get("focused").and_then(|v| v.as_bool()).unwrap_or(false);
|
||||||
let minimized = w.get("minimized").and_then(|v| v.as_bool()).unwrap_or(false);
|
let minimized = w
|
||||||
|
.get("minimized")
|
||||||
|
.and_then(|v| v.as_bool())
|
||||||
|
.unwrap_or(false);
|
||||||
let x = w.get("x").and_then(|v| v.as_i64()).unwrap_or(0);
|
let x = w.get("x").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||||
let y = w.get("y").and_then(|v| v.as_i64()).unwrap_or(0);
|
let y = w.get("y").and_then(|v| v.as_i64()).unwrap_or(0);
|
||||||
let width = w.get("width").and_then(|v| v.as_u64()).unwrap_or(0);
|
let width = w.get("width").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||||
let height = w.get("height").and_then(|v| v.as_u64()).unwrap_or(0);
|
let height = w.get("height").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||||
let state = if focused { "focused" } else if minimized { "hidden" } else { "visible" };
|
let state = if focused {
|
||||||
|
"focused"
|
||||||
|
} else if minimized {
|
||||||
|
"hidden"
|
||||||
|
} else {
|
||||||
|
"visible"
|
||||||
|
};
|
||||||
let display_title = if title.len() > 30 {
|
let display_title = if title.len() > 30 {
|
||||||
format!("{}...", &title[..27])
|
format!("{}...", &title[..27])
|
||||||
} else {
|
} else {
|
||||||
title.to_string()
|
title.to_string()
|
||||||
};
|
};
|
||||||
println!("@{:<4} {:<30} ({:<7}) {},{} {}x{}", ref_id, display_title, state, x, y, width, height);
|
println!(
|
||||||
|
"@{:<4} {:<30} ({:<7}) {},{} {}x{}",
|
||||||
|
ref_id, display_title, state, x, y, width, height
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,14 @@ pub struct Response {
|
||||||
impl Request {
|
impl Request {
|
||||||
pub fn new(action: &str) -> Self {
|
pub fn new(action: &str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: format!("r{}", std::time::SystemTime::now()
|
id: format!(
|
||||||
|
"r{}",
|
||||||
|
std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.as_micros() % 1_000_000),
|
.as_micros()
|
||||||
|
% 1_000_000
|
||||||
|
),
|
||||||
action: action.to_string(),
|
action: action.to_string(),
|
||||||
extra: Value::Object(serde_json::Map::new()),
|
extra: Value::Object(serde_json::Map::new()),
|
||||||
}
|
}
|
||||||
|
|
@ -40,10 +44,18 @@ impl Request {
|
||||||
|
|
||||||
impl Response {
|
impl Response {
|
||||||
pub fn ok(data: Value) -> Self {
|
pub fn ok(data: Value) -> Self {
|
||||||
Self { success: true, data: Some(data), error: None }
|
Self {
|
||||||
|
success: true,
|
||||||
|
data: Some(data),
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn err(msg: impl Into<String>) -> Self {
|
pub fn err(msg: impl Into<String>) -> Self {
|
||||||
Self { success: false, data: None, error: Some(msg.into()) }
|
Self {
|
||||||
|
success: false,
|
||||||
|
data: None,
|
||||||
|
error: Some(msg.into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|
@ -26,7 +26,10 @@ pub struct RefMap {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
impl RefMap {
|
impl RefMap {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { map: HashMap::new(), next_ref: 1 }
|
Self {
|
||||||
|
map: HashMap::new(),
|
||||||
|
next_ref: 1,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
|
|
@ -57,16 +60,14 @@ impl RefMap {
|
||||||
// Try substring match on app_class or title (case-insensitive)
|
// Try substring match on app_class or title (case-insensitive)
|
||||||
let lower = selector.to_lowercase();
|
let lower = selector.to_lowercase();
|
||||||
self.map.values().find(|e| {
|
self.map.values().find(|e| {
|
||||||
e.app_class.to_lowercase().contains(&lower)
|
e.app_class.to_lowercase().contains(&lower) || e.title.to_lowercase().contains(&lower)
|
||||||
|| e.title.to_lowercase().contains(&lower)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve a selector to the center coordinates of the window.
|
/// Resolve a selector to the center coordinates of the window.
|
||||||
pub fn resolve_to_center(&self, selector: &str) -> Option<(i32, i32)> {
|
pub fn resolve_to_center(&self, selector: &str) -> Option<(i32, i32)> {
|
||||||
self.resolve(selector).map(|e| {
|
self.resolve(selector)
|
||||||
(e.x + e.width as i32 / 2, e.y + e.height as i32 / 2)
|
.map(|e| (e.x + e.width as i32 / 2, e.y + e.height as i32 / 2))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn entries(&self) -> impl Iterator<Item = (&String, &RefEntry)> {
|
pub fn entries(&self) -> impl Iterator<Item = (&String, &RefEntry)> {
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
use super::state::DaemonState;
|
||||||
use crate::backend::DesktopBackend;
|
use crate::backend::DesktopBackend;
|
||||||
use crate::core::protocol::{Request, Response};
|
use crate::core::protocol::{Request, Response};
|
||||||
use crate::core::refs::RefEntry;
|
use crate::core::refs::RefEntry;
|
||||||
use super::state::DaemonState;
|
|
||||||
|
|
||||||
pub async fn handle_request(
|
pub async fn handle_request(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
||||||
request: &Request,
|
|
||||||
state: &Arc<Mutex<DaemonState>>,
|
|
||||||
) -> Response {
|
|
||||||
match request.action.as_str() {
|
match request.action.as_str() {
|
||||||
"snapshot" => handle_snapshot(request, state).await,
|
"snapshot" => handle_snapshot(request, state).await,
|
||||||
"click" => handle_click(request, state).await,
|
"click" => handle_click(request, state).await,
|
||||||
|
|
@ -33,10 +30,7 @@ pub async fn handle_request(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_snapshot(
|
async fn handle_snapshot(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
||||||
request: &Request,
|
|
||||||
state: &Arc<Mutex<DaemonState>>,
|
|
||||||
) -> Response {
|
|
||||||
let annotate = request
|
let annotate = request
|
||||||
.extra
|
.extra
|
||||||
.get("annotate")
|
.get("annotate")
|
||||||
|
|
@ -70,10 +64,7 @@ async fn handle_snapshot(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_click(
|
async fn handle_click(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
||||||
request: &Request,
|
|
||||||
state: &Arc<Mutex<DaemonState>>,
|
|
||||||
) -> Response {
|
|
||||||
let selector = match request.extra.get("selector").and_then(|v| v.as_str()) {
|
let selector = match request.extra.get("selector").and_then(|v| v.as_str()) {
|
||||||
Some(s) => s.to_string(),
|
Some(s) => s.to_string(),
|
||||||
None => return Response::err("Missing 'selector' field"),
|
None => return Response::err("Missing 'selector' field"),
|
||||||
|
|
@ -92,19 +83,16 @@ async fn handle_click(
|
||||||
// Resolve as window ref
|
// Resolve as window ref
|
||||||
match state.ref_map.resolve_to_center(&selector) {
|
match state.ref_map.resolve_to_center(&selector) {
|
||||||
Some((x, y)) => match state.backend.click(x, y) {
|
Some((x, y)) => match state.backend.click(x, y) {
|
||||||
Ok(()) => Response::ok(
|
Ok(()) => {
|
||||||
serde_json::json!({"clicked": {"x": x, "y": y, "ref": selector}}),
|
Response::ok(serde_json::json!({"clicked": {"x": x, "y": y, "ref": selector}}))
|
||||||
),
|
}
|
||||||
Err(e) => Response::err(format!("Click failed: {e}")),
|
Err(e) => Response::err(format!("Click failed: {e}")),
|
||||||
},
|
},
|
||||||
None => Response::err(format!("Could not resolve selector: {selector}")),
|
None => Response::err(format!("Could not resolve selector: {selector}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_dblclick(
|
async fn handle_dblclick(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
||||||
request: &Request,
|
|
||||||
state: &Arc<Mutex<DaemonState>>,
|
|
||||||
) -> Response {
|
|
||||||
let selector = match request.extra.get("selector").and_then(|v| v.as_str()) {
|
let selector = match request.extra.get("selector").and_then(|v| v.as_str()) {
|
||||||
Some(s) => s.to_string(),
|
Some(s) => s.to_string(),
|
||||||
None => return Response::err("Missing 'selector' field"),
|
None => return Response::err("Missing 'selector' field"),
|
||||||
|
|
@ -130,10 +118,7 @@ async fn handle_dblclick(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_type(
|
async fn handle_type(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
||||||
request: &Request,
|
|
||||||
state: &Arc<Mutex<DaemonState>>,
|
|
||||||
) -> Response {
|
|
||||||
let text = match request.extra.get("text").and_then(|v| v.as_str()) {
|
let text = match request.extra.get("text").and_then(|v| v.as_str()) {
|
||||||
Some(t) => t.to_string(),
|
Some(t) => t.to_string(),
|
||||||
None => return Response::err("Missing 'text' field"),
|
None => return Response::err("Missing 'text' field"),
|
||||||
|
|
@ -147,10 +132,7 @@ async fn handle_type(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_press(
|
async fn handle_press(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
||||||
request: &Request,
|
|
||||||
state: &Arc<Mutex<DaemonState>>,
|
|
||||||
) -> Response {
|
|
||||||
let key = match request.extra.get("key").and_then(|v| v.as_str()) {
|
let key = match request.extra.get("key").and_then(|v| v.as_str()) {
|
||||||
Some(k) => k.to_string(),
|
Some(k) => k.to_string(),
|
||||||
None => return Response::err("Missing 'key' field"),
|
None => return Response::err("Missing 'key' field"),
|
||||||
|
|
@ -164,10 +146,7 @@ async fn handle_press(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_hotkey(
|
async fn handle_hotkey(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
||||||
request: &Request,
|
|
||||||
state: &Arc<Mutex<DaemonState>>,
|
|
||||||
) -> Response {
|
|
||||||
let keys: Vec<String> = match request.extra.get("keys").and_then(|v| v.as_array()) {
|
let keys: Vec<String> = match request.extra.get("keys").and_then(|v| v.as_array()) {
|
||||||
Some(arr) => arr
|
Some(arr) => arr
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -184,10 +163,7 @@ async fn handle_hotkey(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_mouse_move(
|
async fn handle_mouse_move(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
||||||
request: &Request,
|
|
||||||
state: &Arc<Mutex<DaemonState>>,
|
|
||||||
) -> Response {
|
|
||||||
let x = match request.extra.get("x").and_then(|v| v.as_i64()) {
|
let x = match request.extra.get("x").and_then(|v| v.as_i64()) {
|
||||||
Some(v) => v as i32,
|
Some(v) => v as i32,
|
||||||
None => return Response::err("Missing 'x' field"),
|
None => return Response::err("Missing 'x' field"),
|
||||||
|
|
@ -205,10 +181,7 @@ async fn handle_mouse_move(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_mouse_scroll(
|
async fn handle_mouse_scroll(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
||||||
request: &Request,
|
|
||||||
state: &Arc<Mutex<DaemonState>>,
|
|
||||||
) -> Response {
|
|
||||||
let amount = match request.extra.get("amount").and_then(|v| v.as_i64()) {
|
let amount = match request.extra.get("amount").and_then(|v| v.as_i64()) {
|
||||||
Some(v) => v as i32,
|
Some(v) => v as i32,
|
||||||
None => return Response::err("Missing 'amount' field"),
|
None => return Response::err("Missing 'amount' field"),
|
||||||
|
|
@ -223,17 +196,12 @@ async fn handle_mouse_scroll(
|
||||||
let mut state = state.lock().await;
|
let mut state = state.lock().await;
|
||||||
|
|
||||||
match state.backend.scroll(amount, &axis) {
|
match state.backend.scroll(amount, &axis) {
|
||||||
Ok(()) => {
|
Ok(()) => Response::ok(serde_json::json!({"scrolled": {"amount": amount, "axis": axis}})),
|
||||||
Response::ok(serde_json::json!({"scrolled": {"amount": amount, "axis": axis}}))
|
|
||||||
}
|
|
||||||
Err(e) => Response::err(format!("Scroll failed: {e}")),
|
Err(e) => Response::err(format!("Scroll failed: {e}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_mouse_drag(
|
async fn handle_mouse_drag(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
||||||
request: &Request,
|
|
||||||
state: &Arc<Mutex<DaemonState>>,
|
|
||||||
) -> Response {
|
|
||||||
let x1 = match request.extra.get("x1").and_then(|v| v.as_i64()) {
|
let x1 = match request.extra.get("x1").and_then(|v| v.as_i64()) {
|
||||||
Some(v) => v as i32,
|
Some(v) => v as i32,
|
||||||
None => return Response::err("Missing 'x1' field"),
|
None => return Response::err("Missing 'x1' field"),
|
||||||
|
|
@ -297,10 +265,7 @@ async fn handle_window_action(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_move_window(
|
async fn handle_move_window(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
||||||
request: &Request,
|
|
||||||
state: &Arc<Mutex<DaemonState>>,
|
|
||||||
) -> Response {
|
|
||||||
let selector = match request.extra.get("selector").and_then(|v| v.as_str()) {
|
let selector = match request.extra.get("selector").and_then(|v| v.as_str()) {
|
||||||
Some(s) => s.to_string(),
|
Some(s) => s.to_string(),
|
||||||
None => return Response::err("Missing 'selector' field"),
|
None => return Response::err("Missing 'selector' field"),
|
||||||
|
|
@ -322,16 +287,21 @@ async fn handle_move_window(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_resize_window(
|
async fn handle_resize_window(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
||||||
request: &Request,
|
|
||||||
state: &Arc<Mutex<DaemonState>>,
|
|
||||||
) -> Response {
|
|
||||||
let selector = match request.extra.get("selector").and_then(|v| v.as_str()) {
|
let selector = match request.extra.get("selector").and_then(|v| v.as_str()) {
|
||||||
Some(s) => s.to_string(),
|
Some(s) => s.to_string(),
|
||||||
None => return Response::err("Missing 'selector' field"),
|
None => return Response::err("Missing 'selector' field"),
|
||||||
};
|
};
|
||||||
let w = request.extra.get("w").and_then(|v| v.as_u64()).unwrap_or(800) as u32;
|
let w = request
|
||||||
let h = request.extra.get("h").and_then(|v| v.as_u64()).unwrap_or(600) as u32;
|
.extra
|
||||||
|
.get("w")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.unwrap_or(800) as u32;
|
||||||
|
let h = request
|
||||||
|
.extra
|
||||||
|
.get("h")
|
||||||
|
.and_then(|v| v.as_u64())
|
||||||
|
.unwrap_or(600) as u32;
|
||||||
|
|
||||||
let mut state = state.lock().await;
|
let mut state = state.lock().await;
|
||||||
let entry = match state.ref_map.resolve(&selector) {
|
let entry = match state.ref_map.resolve(&selector) {
|
||||||
|
|
@ -347,9 +317,7 @@ async fn handle_resize_window(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_list_windows(
|
async fn handle_list_windows(state: &Arc<Mutex<DaemonState>>) -> Response {
|
||||||
state: &Arc<Mutex<DaemonState>>,
|
|
||||||
) -> Response {
|
|
||||||
let mut state = state.lock().await;
|
let mut state = state.lock().await;
|
||||||
// Re-run snapshot without screenshot, just to get current window list
|
// Re-run snapshot without screenshot, just to get current window list
|
||||||
match state.backend.snapshot(false) {
|
match state.backend.snapshot(false) {
|
||||||
|
|
@ -392,10 +360,7 @@ async fn handle_get_mouse_position(state: &Arc<Mutex<DaemonState>>) -> Response
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_screenshot(
|
async fn handle_screenshot(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
||||||
request: &Request,
|
|
||||||
state: &Arc<Mutex<DaemonState>>,
|
|
||||||
) -> Response {
|
|
||||||
let annotate = request
|
let annotate = request
|
||||||
.extra
|
.extra
|
||||||
.get("annotate")
|
.get("annotate")
|
||||||
|
|
@ -420,10 +385,7 @@ async fn handle_screenshot(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_launch(
|
async fn handle_launch(request: &Request, state: &Arc<Mutex<DaemonState>>) -> Response {
|
||||||
request: &Request,
|
|
||||||
state: &Arc<Mutex<DaemonState>>,
|
|
||||||
) -> Response {
|
|
||||||
let command = match request.extra.get("command").and_then(|v| v.as_str()) {
|
let command = match request.extra.get("command").and_then(|v| v.as_str()) {
|
||||||
Some(c) => c.to_string(),
|
Some(c) => c.to_string(),
|
||||||
None => return Response::err("Missing 'command' field"),
|
None => return Response::err("Missing 'command' field"),
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,7 @@ async fn async_run() -> Result<()> {
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.context("DESKCTL_SOCKET_PATH not set")?;
|
.context("DESKCTL_SOCKET_PATH not set")?;
|
||||||
|
|
||||||
let pid_path = std::env::var("DESKCTL_PID_PATH")
|
let pid_path = std::env::var("DESKCTL_PID_PATH").map(PathBuf::from).ok();
|
||||||
.map(PathBuf::from)
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
// Clean up stale socket
|
// Clean up stale socket
|
||||||
if socket_path.exists() {
|
if socket_path.exists() {
|
||||||
|
|
@ -48,7 +46,7 @@ async fn async_run() -> Result<()> {
|
||||||
let session = std::env::var("DESKCTL_SESSION").unwrap_or_else(|_| "default".to_string());
|
let session = std::env::var("DESKCTL_SESSION").unwrap_or_else(|_| "default".to_string());
|
||||||
let state = Arc::new(Mutex::new(
|
let state = Arc::new(Mutex::new(
|
||||||
DaemonState::new(session, socket_path.clone())
|
DaemonState::new(session, socket_path.clone())
|
||||||
.context("Failed to initialize daemon state")?
|
.context("Failed to initialize daemon state")?,
|
||||||
));
|
));
|
||||||
|
|
||||||
let shutdown = Arc::new(tokio::sync::Notify::new());
|
let shutdown = Arc::new(tokio::sync::Notify::new());
|
||||||
|
|
@ -111,9 +109,8 @@ async fn handle_connection(
|
||||||
// exits even if the client has already closed the connection.
|
// exits even if the client has already closed the connection.
|
||||||
if request.action == "shutdown" {
|
if request.action == "shutdown" {
|
||||||
shutdown.notify_one();
|
shutdown.notify_one();
|
||||||
let response = crate::core::protocol::Response::ok(
|
let response =
|
||||||
serde_json::json!({"message": "Shutting down"})
|
crate::core::protocol::Response::ok(serde_json::json!({"message": "Shutting down"}));
|
||||||
);
|
|
||||||
let json = serde_json::to_string(&response)?;
|
let json = serde_json::to_string(&response)?;
|
||||||
// Ignore write errors - client may have already closed the connection.
|
// Ignore write errors - client may have already closed the connection.
|
||||||
let _ = writer.write_all(format!("{json}\n").as_bytes()).await;
|
let _ = writer.write_all(format!("{json}\n").as_bytes()).await;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue