1use std::collections::HashMap;
19use std::sync::mpsc;
20use std::sync::{Mutex, OnceLock};
21use std::thread;
22use std::time::Duration;
23use std::io::Cursor;
24use std::io::Read;
25
26use smithay::input::keyboard::xkb;
27
28use base64::Engine;
29use base64::engine::general_purpose::STANDARD as BASE64;
30use image::{ImageBuffer, Rgba, ImageFormat};
31use serde::Deserialize;
32use tiny_http;
33
34use crate::wayland::keymap::keysym_for_char;
35use crate::ThreadCommand;
36
37fn clamp<T: PartialOrd>(v: T, lo: T, hi: T) -> T {
38 if v < lo { lo } else if v > hi { hi } else { v }
39}
40
41pub fn encode_png_rgba(data: &[u8], width: u32, height: u32) -> Result<Vec<u8>, String> {
47 let img = ImageBuffer::<Rgba<u8>, _>::from_raw(width, height, data.to_vec())
48 .ok_or("Failed to create image buffer")?;
49 let mut png = Vec::new();
50 img.write_to(&mut Cursor::new(&mut png), ImageFormat::Png)
51 .map_err(|e| format!("PNG encode error: {}", e))?;
52 Ok(png)
53}
54
55fn scancode_for_keyname(name: &str) -> Option<u32> {
56 Some(match name.to_lowercase().as_str() {
57 "return" | "enter" => 36,
58 "tab" => 23,
59 "escape" | "esc" => 9,
60 "backspace" => 22,
61 "delete" => 119,
62 "insert" => 118,
63 "home" => 110,
64 "end" => 115,
65 "pageup" => 112,
66 "pagedown" => 117,
67 "up" => 111,
68 "down" => 116,
69 "left" => 113,
70 "right" => 114,
71 "space" => 65,
72 "capslock" => 66,
73 "numlock" => 77,
74 "scrolllock" => 78,
75 "printscreen" | "sysrq" | "print" => 107,
76 "pause" | "break" => 127,
77 "f1" => 67, "f2" => 68, "f3" => 69, "f4" => 70,
78 "f5" => 71, "f6" => 72, "f7" => 73, "f8" => 74,
79 "f9" => 75, "f10" => 76, "f11" => 95, "f12" => 96,
80 "f13" => 191, "f14" => 192, "f15" => 193, "f16" => 194,
81 "f17" => 195, "f18" => 196, "f19" => 197, "f20" => 198,
82 "f21" => 199, "f22" => 200, "f23" => 201, "f24" => 202,
83 "ctrl" | "lctrl" | "leftctrl" => 37,
84 "rctrl" | "rightctrl" => 105,
85 "shift" | "lshift" | "leftshift" => 50,
86 "rshift" | "rightshift" => 62,
87 "alt" | "lalt" | "leftalt" => 64,
88 "ralt" | "rightalt" => 108,
89 "super" | "meta" | "lsuper" | "leftmeta" | "leftsuper" | "windows" | "leftwindows" => 133,
90 "rsuper" | "rightmeta" | "rightsuper" | "rightwindows" => 134,
91 "menu" | "compose" => 135,
92 "kp_0" | "kp0" => 90, "kp_1" | "kp1" => 87,
93 "kp_2" | "kp2" => 88, "kp_3" | "kp3" => 89,
94 "kp_4" | "kp4" => 83, "kp_5" | "kp5" => 84,
95 "kp_6" | "kp6" => 85, "kp_7" | "kp7" => 79,
96 "kp_8" | "kp8" => 80, "kp_9" | "kp9" => 81,
97 "kp_decimal" | "kp_dot" => 91,
98 "kp_divide" | "kp_slash" => 106,
99 "kp_multiply" | "kp_asterisk" => 63,
100 "kp_subtract" | "kp_minus" => 82,
101 "kp_add" | "kp_plus" => 86,
102 "kp_enter" => 104,
103 _ => return None,
104 })
105}
106
107fn keysym_for_key_spec(name: &str) -> u32 {
112 let mut chars = name.chars();
113 if let (Some(c), None) = (chars.next(), chars.next()) {
114 return keysym_for_char(c);
115 }
116 let sym = xkb::keysym_from_name(name, xkb::KEYSYM_NO_FLAGS).raw();
117 if sym != 0 {
118 return sym;
119 }
120 xkb::keysym_from_name(name, xkb::KEYSYM_CASE_INSENSITIVE).raw()
121}
122
123fn is_modifier(scancode: u32) -> bool {
124 matches!(scancode, 37 | 105 | 50 | 62 | 64 | 108 | 133 | 134)
125}
126
127#[derive(Clone, Copy)]
130pub enum CuButton {
131 Left,
132 Right,
133 Middle,
134}
135
136pub trait CuBackend {
140 fn name(&self) -> &'static str;
141 fn fb_size(&self) -> Result<(i32, i32), String>;
142 fn display_fb_size(&self, display: u32) -> Result<(i32, i32), String> {
145 if display == 0 {
146 self.fb_size()
147 } else {
148 Err(format!("Unknown display: {display}"))
149 }
150 }
151 fn key(&self, scancode: u32, pressed: bool);
152 fn mouse_move(&self, x: f64, y: f64);
153 fn button(&self, btn: CuButton, pressed: bool);
154 fn scroll(&self, dx: f64, dy: f64);
155 fn screenshot_png(&self, display: u32) -> Result<Vec<u8>, String>;
157 fn cursor_pos(&self) -> Result<(f64, f64), String>;
158 fn with_transient_keysyms(&self, keysyms: &[u32], seq: &mut dyn FnMut(&HashMap<u32, u32>)) {
166 let _ = keysyms;
167 seq(&HashMap::new());
168 }
169 fn resolve_keysyms(&self, keysyms: &[u32]) -> Vec<(u32, u32)>;
175 fn altgr_keycode(&self) -> u32 {
180 KC_ALTGR
181 }
182}
183
184const BTN_LEFT: u32 = 0x110;
185const BTN_RIGHT: u32 = 0x111;
186const BTN_MIDDLE: u32 = 0x112;
187
188const REPLY_TIMEOUT: Duration = Duration::from_secs(5);
192
193pub struct CuWaylandBackend {
196 tx: smithay::reexports::calloop::channel::Sender<ThreadCommand>,
197}
198
199impl CuBackend for CuWaylandBackend {
200 fn name(&self) -> &'static str {
201 "wayland"
202 }
203
204 fn fb_size(&self) -> Result<(i32, i32), String> {
205 self.display_fb_size(0)
206 }
207
208 fn display_fb_size(&self, display: u32) -> Result<(i32, i32), String> {
209 let (resp_tx, resp_rx) = mpsc::channel();
210 self.tx.send(ThreadCommand::CuGetInfo { display_id: display, resp: resp_tx })
211 .map_err(|_| "Failed to request compositor info".to_string())?;
212 let (w, h, _) = resp_rx.recv_timeout(REPLY_TIMEOUT)
213 .map_err(|_| "Compositor info request failed".to_string())?;
214 if w <= 0 || h <= 0 {
217 return Err(format!("Unknown display: {display}"));
218 }
219 Ok((w, h))
220 }
221
222 fn key(&self, scancode: u32, pressed: bool) {
223 let _ = self.tx.send(ThreadCommand::KeyboardKey {
224 scancode,
225 state: if pressed { 1 } else { 0 },
226 });
227 }
228
229 fn mouse_move(&self, x: f64, y: f64) {
230 let _ = self.tx.send(ThreadCommand::PointerMotion { x, y });
231 }
232
233 fn button(&self, btn: CuButton, pressed: bool) {
234 let btn = match btn {
235 CuButton::Left => BTN_LEFT,
236 CuButton::Right => BTN_RIGHT,
237 CuButton::Middle => BTN_MIDDLE,
238 };
239 let _ = self.tx.send(ThreadCommand::PointerButton {
240 btn,
241 state: if pressed { 1 } else { 0 },
242 });
243 }
244
245 fn scroll(&self, dx: f64, dy: f64) {
246 let _ = self.tx.send(ThreadCommand::PointerAxis { x: dx, y: dy });
247 }
248
249 fn screenshot_png(&self, display: u32) -> Result<Vec<u8>, String> {
250 let (resp_tx, resp_rx) = mpsc::channel();
251 self.tx.send(ThreadCommand::CuScreenshot { display_id: display, resp: resp_tx })
252 .map_err(|_| "Failed to request screenshot".to_string())?;
253 resp_rx.recv_timeout(REPLY_TIMEOUT).map_err(|_| "Screenshot failed".to_string())?
254 }
255
256 fn cursor_pos(&self) -> Result<(f64, f64), String> {
257 let (resp_tx, resp_rx) = mpsc::channel();
258 self.tx.send(ThreadCommand::CuCursorPosition { resp: resp_tx })
259 .map_err(|_| "Failed to request cursor position".to_string())?;
260 resp_rx.recv_timeout(REPLY_TIMEOUT).map_err(|_| "Cursor position failed".to_string())
261 }
262
263 fn resolve_keysyms(&self, keysyms: &[u32]) -> Vec<(u32, u32)> {
264 let (resp_tx, resp_rx) = mpsc::channel();
265 if self
266 .tx
267 .send(ThreadCommand::BindKeysyms { keysyms: keysyms.to_vec(), reply: resp_tx })
268 .is_err()
269 {
270 return vec![(0, 0); keysyms.len()];
271 }
272 resp_rx
273 .recv_timeout(REPLY_TIMEOUT)
274 .unwrap_or_else(|_| vec![(0, 0); keysyms.len()])
275 }
276}
277
278struct KeyResolver<'a> {
283 backend: &'a dyn CuBackend,
284 cache: HashMap<u32, (u32, u32)>,
285}
286
287impl<'a> KeyResolver<'a> {
288 fn new(backend: &'a dyn CuBackend) -> Self {
289 Self { backend, cache: HashMap::new() }
290 }
291
292 fn prefetch(&mut self, keysyms: &[u32]) {
294 let missing: Vec<u32> = {
295 let mut seen = std::collections::HashSet::new();
296 keysyms
297 .iter()
298 .copied()
299 .filter(|&s| s != 0 && !self.cache.contains_key(&s) && seen.insert(s))
300 .collect()
301 };
302 if missing.is_empty() {
303 return;
304 }
305 let out = self.backend.resolve_keysyms(&missing);
306 for (sym, hit) in missing.into_iter().zip(out) {
307 self.cache.insert(sym, hit);
308 }
309 }
310
311 fn resolve(&mut self, keysym: u32) -> Option<(u32, u32)> {
314 if keysym == 0 {
315 return None;
316 }
317 self.prefetch(&[keysym]);
318 let hit = self.cache.get(&keysym).copied().unwrap_or((0, 0));
319 (hit.0 != 0).then_some(hit)
320 }
321}
322
323const KC_SHIFT: u32 = 50;
326const KC_ALTGR: u32 = 108;
327
328fn level_modifiers(level: u32, held_mask: u32, altgr: u32) -> Vec<u32> {
332 let mut out = Vec::new();
333 if level & 1 != 0 && held_mask & 1 == 0 {
334 out.push(KC_SHIFT);
335 }
336 if level & 2 != 0 && held_mask & 2 == 0 && altgr != 0 {
337 out.push(altgr);
338 }
339 out
340}
341
342#[derive(Deserialize)]
343struct CuActionRequest {
344 action: String,
345 coordinate: Option<[f64; 2]>,
346 start_coordinate: Option<[f64; 2]>,
347 text: Option<String>,
348 key: Option<String>,
349 scroll_direction: Option<String>,
350 scroll_amount: Option<i32>,
351 duration: Option<f64>,
352 region: Option<[f64; 4]>,
353 display: Option<u32>,
356}
357
358fn ok_json() -> String {
359 "{\"result\":\"ok\"}".to_string()
360}
361
362fn handle_action(req: CuActionRequest, backend: &dyn CuBackend) -> String {
363 let result = handle_action_inner(req, backend);
364 match result {
365 Ok(response) => response,
366 Err(e) => format!("{{\"error\":\"{}\"}}", e.replace('"', "\\\"")),
367 }
368}
369
370fn handle_action_inner(req: CuActionRequest, b: &dyn CuBackend) -> Result<String, String> {
381 let sleep_ms = |ms: u64| thread::sleep(Duration::from_millis(ms));
382
383 let (fb_w, fb_h) = b.fb_size()?;
384
385 let handle_coord = |coord: [f64; 2]| -> (f64, f64) {
386 (
387 clamp(coord[0], 0.0, (fb_w - 1) as f64),
388 clamp(coord[1], 0.0, (fb_h - 1) as f64),
389 )
390 };
391
392 let handle_modifier = |mod_name: &str| -> Option<u32> {
393 let sc = scancode_for_keyname(mod_name)?;
394 if is_modifier(sc) { Some(sc) } else { None }
395 };
396
397 match req.action.as_str() {
398 "screenshot" => {
399 let png = b.screenshot_png(req.display.unwrap_or(0))?;
400 let b64 = BASE64.encode(&png);
401 Ok(format!("{{\"data\":\"{}\"}}", b64))
402 }
403
404 "mouse_move" => {
405 let coord = req.coordinate.ok_or("Missing coordinate")?;
406 let (fx, fy) = handle_coord(coord);
407 b.mouse_move(fx, fy);
408 Ok(ok_json())
409 }
410
411 "left_click" | "right_click" | "middle_click" => {
412 let btn = match req.action.as_str() {
413 "left_click" => CuButton::Left,
414 "right_click" => CuButton::Right,
415 _ => CuButton::Middle,
416 };
417 if let Some(coord) = req.coordinate {
418 let (fx, fy) = handle_coord(coord);
419 b.mouse_move(fx, fy);
420 sleep_ms(30);
421 }
422 if let Some(ref mod_name) = req.text
423 && let Some(sc) = handle_modifier(mod_name) {
424 b.key(sc, true);
425 sleep_ms(20);
426 }
427 b.button(btn, true);
428 sleep_ms(20);
429 b.button(btn, false);
430 if let Some(ref mod_name) = req.text
431 && let Some(sc) = handle_modifier(mod_name) {
432 sleep_ms(10);
433 b.key(sc, false);
434 }
435 Ok(ok_json())
436 }
437
438 "double_click" | "triple_click" => {
439 let n = if req.action == "double_click" { 2 } else { 3 };
440 if let Some(coord) = req.coordinate {
441 let (fx, fy) = handle_coord(coord);
442 b.mouse_move(fx, fy);
443 sleep_ms(30);
444 }
445 if let Some(ref mod_name) = req.text
446 && let Some(sc) = handle_modifier(mod_name) {
447 b.key(sc, true);
448 sleep_ms(20);
449 }
450 for _ in 0..n {
451 b.button(CuButton::Left, true);
452 sleep_ms(10);
453 b.button(CuButton::Left, false);
454 sleep_ms(10);
455 }
456 if let Some(ref mod_name) = req.text
457 && let Some(sc) = handle_modifier(mod_name) {
458 sleep_ms(10);
459 b.key(sc, false);
460 }
461 Ok(ok_json())
462 }
463
464 "left_click_drag" => {
465 let start = req.start_coordinate.ok_or("Missing start_coordinate")?;
466 let end = req.coordinate.ok_or("Missing coordinate")?;
467 let (sx, sy) = handle_coord(start);
468 let (ex, ey) = handle_coord(end);
469 b.mouse_move(sx, sy);
470 sleep_ms(30);
471 b.button(CuButton::Left, true);
472 sleep_ms(30);
473 b.mouse_move(ex, ey);
474 sleep_ms(30);
475 b.button(CuButton::Left, false);
476 Ok(ok_json())
477 }
478
479 "left_mouse_down" => {
480 b.button(CuButton::Left, true);
481 Ok(ok_json())
482 }
483
484 "left_mouse_up" => {
485 b.button(CuButton::Left, false);
486 Ok(ok_json())
487 }
488
489 "type" => {
490 let text = req.text.as_deref().ok_or("Missing text")?;
491 if b.name() == "wayland"
496 && let Some(sock) = app_wayland_socket_path() {
497 static FAILED_SOCK: Mutex<Option<String>> = Mutex::new(None);
501 match crate::wayland::vkclient::type_text_to(&sock, text) {
502 Ok(()) => {
503 *FAILED_SOCK.lock().unwrap() = None;
504 return Ok(ok_json());
505 }
506 Err(e) => {
507 let mut last = FAILED_SOCK.lock().unwrap();
508 if last.as_deref() != Some(sock.as_str()) {
509 eprintln!(
510 "[ComputerUse] app-compositor type via {sock} failed ({e}); using local seat until it is reachable"
511 );
512 *last = Some(sock);
513 }
514 }
515 }
516 }
517 let mut resolver = KeyResolver::new(b);
518 let syms: Vec<u32> = text.chars().map(keysym_for_char).collect();
519 resolver.prefetch(&syms);
520 let mut unresolved: Vec<u32> = Vec::new();
523 for &sym in &syms {
524 if sym != 0 && resolver.resolve(sym).is_none() && !unresolved.contains(&sym) {
525 unresolved.push(sym);
526 }
527 }
528 b.with_transient_keysyms(&unresolved, &mut |bound| {
529 for (i, &sym) in syms.iter().enumerate() {
530 if i > 0 && i % 50 == 0 {
531 sleep_ms(20);
532 }
533 if let Some((sc, level)) = resolver.resolve(sym) {
534 let level_mods = level_modifiers(level, 0, b.altgr_keycode());
535 for &m in &level_mods {
536 b.key(m, true);
537 sleep_ms(5);
538 }
539 b.key(sc, true);
540 sleep_ms(10);
541 b.key(sc, false);
542 for &m in level_mods.iter().rev() {
543 b.key(m, false);
544 }
545 sleep_ms(8);
546 } else if let Some(&kc) = bound.get(&sym) {
547 b.key(kc, true);
549 sleep_ms(10);
550 b.key(kc, false);
551 sleep_ms(8);
552 }
553 }
554 });
555 Ok(ok_json())
556 }
557
558 "key" => {
559 let text = req.text.as_deref().ok_or("Missing text")?;
560 let mut resolver = KeyResolver::new(b);
561 let mut mods: Vec<u32> = Vec::new();
562 let mut main_key: Option<(u32, u32)> = None;
563 let mut unresolved_sym: Option<u32> = None;
566 for part in text.split('+') {
567 let trimmed = part.trim();
568 if let Some(sc) = scancode_for_keyname(trimmed) {
569 if is_modifier(sc) {
570 mods.push(sc);
571 } else {
572 main_key = Some((sc, 0));
573 unresolved_sym = None;
574 }
575 } else {
576 let sym = keysym_for_key_spec(trimmed);
577 if let Some(hit) = resolver.resolve(sym) {
578 main_key = Some(hit);
579 unresolved_sym = None;
580 } else if sym != 0 {
581 main_key = None;
582 unresolved_sym = Some(sym);
583 }
584 }
585 }
586 let unresolved: Vec<u32> = unresolved_sym.into_iter().collect();
587 b.with_transient_keysyms(&unresolved, &mut |bound| {
588 let main_key = main_key.or_else(|| {
589 unresolved_sym.and_then(|s| bound.get(&s)).map(|&kc| (kc, 0))
591 });
592 for &sc in &mods {
593 b.key(sc, true);
594 sleep_ms(10);
595 }
596 if let Some((sc, level)) = main_key {
597 let altgr = b.altgr_keycode();
601 let held = mods.iter().fold(0u32, |m, &sc| match sc {
602 50 | 62 => m | 1,
603 sc if sc == KC_ALTGR || sc == altgr => m | 2,
604 _ => m,
605 });
606 let level_mods = level_modifiers(level, held, altgr);
607 for &m in &level_mods {
608 b.key(m, true);
609 sleep_ms(10);
610 }
611 b.key(sc, true);
612 sleep_ms(30);
613 b.key(sc, false);
614 for &m in level_mods.iter().rev() {
615 sleep_ms(10);
616 b.key(m, false);
617 }
618 } else if let Some(&last_mod) = mods.last() {
619 b.key(last_mod, true);
620 sleep_ms(30);
621 b.key(last_mod, false);
622 }
623 for &sc in mods.iter().rev() {
624 sleep_ms(10);
625 b.key(sc, false);
626 }
627 });
628 Ok(ok_json())
629 }
630
631 "hold_key" => {
632 let text = req.text.as_deref().ok_or("Missing text")?;
633 let duration = req.duration.ok_or("Missing duration")?;
634 let duration = duration.min(100.0);
635 let trimmed = text.trim();
636 let mut resolver = KeyResolver::new(b);
637 let hit = match scancode_for_keyname(trimmed) {
638 Some(sc) => Some((sc, 0)),
639 None => resolver.resolve(keysym_for_key_spec(trimmed)),
640 };
641 if let Some((sc, level)) = hit {
642 let level_mods = level_modifiers(level, 0, b.altgr_keycode());
643 for &m in &level_mods {
644 b.key(m, true);
645 sleep_ms(10);
646 }
647 b.key(sc, true);
648 sleep_ms((duration * 1000.0) as u64);
649 b.key(sc, false);
650 for &m in level_mods.iter().rev() {
651 sleep_ms(10);
652 b.key(m, false);
653 }
654 }
655 Ok(ok_json())
656 }
657
658 "scroll" => {
659 let dir = req.scroll_direction.as_deref().ok_or("Missing scroll_direction")?;
660 let amount = req.scroll_amount.unwrap_or(1).max(0) as f64;
661 if let Some(coord) = req.coordinate {
662 let (fx, fy) = handle_coord(coord);
663 b.mouse_move(fx, fy);
664 sleep_ms(30);
665 }
666 if let Some(ref mod_name) = req.text
667 && let Some(sc) = handle_modifier(mod_name) {
668 b.key(sc, true);
669 sleep_ms(20);
670 }
671 let (dx, dy) = match dir {
672 "up" => (0.0, -amount),
673 "down" => (0.0, amount),
674 "left" => (-amount, 0.0),
675 "right" => (amount, 0.0),
676 _ => return Err(format!("Invalid scroll_direction: {}", dir)),
677 };
678 b.scroll(dx, dy);
679 sleep_ms(30);
680 if let Some(ref mod_name) = req.text
681 && let Some(sc) = handle_modifier(mod_name) {
682 sleep_ms(10);
683 b.key(sc, false);
684 }
685 Ok(ok_json())
686 }
687
688 "cursor_position" => {
689 let (x, y) = b.cursor_pos()?;
690 Ok(format!("{{\"text\":\"X={},Y={}\"}}", x.round() as i32, y.round() as i32))
691 }
692
693 "wait" => {
694 let duration = req.duration.unwrap_or(1.0).min(100.0);
695 sleep_ms((duration * 1000.0) as u64);
696 Ok(ok_json())
697 }
698
699 "zoom" => {
700 let region = req.region.ok_or("Missing region")?;
701 let display = req.display.unwrap_or(0);
702 let (dw, dh) = b.display_fb_size(display)?;
705 let (x0, y0, x1, y1) = (region[0], region[1], region[2], region[3]);
706 let left = clamp(x0.round() as u32, 0, dw as u32 - 1);
707 let top = clamp(y0.round() as u32, 0, dh as u32 - 1);
708 let right = clamp(x1.round() as u32, left + 1, dw as u32);
709 let bottom = clamp(y1.round() as u32, top + 1, dh as u32);
710 let crop_w = right - left;
711 let crop_h = bottom - top;
712 let png = b.screenshot_png(display)?;
713 if crop_w > 0 && crop_h > 0
714 && let Ok(img) = image::load_from_memory(&png) {
715 let cropped = img.crop_imm(left, top, crop_w, crop_h);
716 let mut out = Vec::new();
717 cropped.write_to(&mut Cursor::new(&mut out), ImageFormat::Png)
718 .map_err(|e| format!("Crop/encode error: {}", e))?;
719 let b64 = BASE64.encode(&out);
720 return Ok(format!("{{\"data\":\"{}\"}}", b64));
721 }
722 let b64 = BASE64.encode(&png);
723 Ok(format!("{{\"data\":\"{}\"}}", b64))
724 }
725
726 _ => Err(format!("Unknown action: {}", req.action)),
727 }
728}
729
730static WAYLAND_TX: Mutex<Option<smithay::reexports::calloop::channel::Sender<ThreadCommand>>> =
731 Mutex::new(None);
732
733#[derive(Deserialize, Default)]
736struct RecordStartRequest {
737 path: Option<String>,
739 display: Option<u32>,
741 fps: Option<f64>,
743 bitrate_kbps: Option<i32>,
745}
746
747fn handle_record_endpoint(url: &str, body: &str) -> Option<String> {
751 let reply = match url {
752 "/record_start" => {
753 let req: RecordStartRequest = if body.trim().is_empty() {
754 RecordStartRequest::default()
755 } else {
756 match serde_json::from_str(body) {
757 Ok(r) => r,
758 Err(e) => return Some(format!("{{\"error\":\"Invalid JSON: {}\"}}", e)),
759 }
760 };
761 let path = req
762 .path
763 .or_else(|| std::env::var("PIXELFLUX_RECORD").ok().filter(|p| !p.is_empty()))
764 .unwrap_or_else(|| {
765 let ts = std::time::SystemTime::now()
766 .duration_since(std::time::UNIX_EPOCH)
767 .map(|d| d.as_secs())
768 .unwrap_or(0);
769 format!("/tmp/pixelflux-record-{ts}.mp4")
770 });
771 let mut opts = crate::recorder::RecordOptions::from_env(path);
772 if let Some(d) = req.display {
773 opts.display_id = d;
774 }
775 if let Some(f) = req.fps {
776 opts.fps = f;
777 }
778 if let Some(b) = req.bitrate_kbps {
779 opts.bitrate_kbps = b;
780 }
781 match crate::recorder::start(opts) {
782 Ok(s) => crate::recorder::status_to_json(&s).to_string(),
783 Err(e) => serde_json::json!({ "error": e }).to_string(),
784 }
785 }
786 "/record_stop" => match crate::recorder::stop() {
787 Ok(s) => {
788 let mut v = crate::recorder::status_to_json(&s);
789 v["stopped"] = serde_json::Value::Bool(true);
790 v.to_string()
791 }
792 Err(e) => serde_json::json!({ "error": e }).to_string(),
793 },
794 "/record_status" => match crate::recorder::status() {
795 Some(s) => crate::recorder::status_to_json(&s).to_string(),
796 None => serde_json::json!({ "active": false }).to_string(),
797 },
798 _ => return None,
799 };
800 Some(reply)
801}
802
803static CU_APP_WAYLAND_DISPLAY: Mutex<Option<String>> = Mutex::new(None);
808
809pub fn set_app_wayland_display(display: Option<String>) {
811 *CU_APP_WAYLAND_DISPLAY.lock().unwrap() = display.filter(|s| !s.is_empty());
812}
813
814fn app_wayland_socket_path() -> Option<String> {
819 let name = CU_APP_WAYLAND_DISPLAY
820 .lock()
821 .unwrap()
822 .clone()
823 .or_else(|| std::env::var("PIXELFLUX_APP_WAYLAND_DISPLAY").ok())
824 .filter(|s| !s.is_empty())?;
825 if crate::wait_socket_name(Duration::from_millis(0)).as_deref() == Some(name.as_str()) {
826 return None;
827 }
828 crate::wayland::wlclient::socket_path(&name)
829}
830
831pub fn register_wayland_backend(tx: smithay::reexports::calloop::channel::Sender<ThreadCommand>) {
834 *WAYLAND_TX.lock().unwrap() = Some(tx);
835}
836
837pub(crate) fn wayland_command_sender(
840) -> Option<smithay::reexports::calloop::channel::Sender<ThreadCommand>> {
841 WAYLAND_TX.lock().unwrap().clone()
842}
843
844pub fn spawn_cu_from_env() {
847 if let Ok(bind) = std::env::var("PIXELFLUX_CU") {
848 start_cu_server(&bind);
849 }
850}
851
852pub fn start_cu_server(bind: &str) {
859 let addr = if bind.contains(':') {
860 bind.to_string()
861 } else {
862 match bind.parse::<u16>() {
863 Ok(port) => format!("0.0.0.0:{port}"),
864 Err(_) => {
865 println!("[ComputerUse] Invalid bind '{bind}' - expected a port or host:port");
866 return;
867 }
868 }
869 };
870 static SPAWNED: OnceLock<()> = OnceLock::new();
871 let mut first = false;
872 SPAWNED.get_or_init(|| {
873 first = true;
874 });
875 if first {
876 thread::spawn(move || run_cu_server(addr));
877 }
878}
879
880fn resolve_backend() -> Result<Box<dyn CuBackend>, String> {
884 if let Some(tx) = WAYLAND_TX.lock().unwrap().clone() {
885 return Ok(Box::new(CuWaylandBackend { tx }));
886 }
887 crate::x11::computer_use::CuX11Backend::connect()
888 .map(|be| Box::new(be) as Box<dyn CuBackend>)
889}
890
891pub fn run_cu_server(addr: String) {
901 println!("[ComputerUse] Server listening on {}", addr);
902
903 let server = match tiny_http::Server::http(addr.as_str()) {
904 Ok(s) => s,
905 Err(e) => {
906 eprintln!("[ComputerUse] Failed to start server: {}", e);
907 return;
908 }
909 };
910
911 let mut last_backend = "";
912 for mut request in server.incoming_requests() {
913 const MAX_CU_BODY: u64 = 4 * 1024 * 1024;
916 let mut body = String::new();
917 if let Err(e) = request
918 .as_reader()
919 .take(MAX_CU_BODY + 1)
920 .read_to_string(&mut body)
921 {
922 let _ = request.respond(tiny_http::Response::from_string(format!(
923 "{{\"error\":\"{}\"}}", e
924 ))
925 .with_status_code(400)
926 .with_header(
927 "Content-Type: application/json".parse::<tiny_http::Header>().unwrap()
928 ));
929 continue;
930 }
931
932 if body.len() as u64 > MAX_CU_BODY {
933 let resp = tiny_http::Response::from_string(
934 "{\"error\":\"request body too large\"}".to_string(),
935 )
936 .with_status_code(413)
937 .with_header(
938 "Content-Type: application/json".parse::<tiny_http::Header>().unwrap()
939 );
940 let _ = request.respond(resp);
941 continue;
942 }
943
944 if let Some(json) = handle_record_endpoint(request.url(), &body) {
945 let _ = request.respond(
946 tiny_http::Response::from_string(json)
947 .with_status_code(200)
948 .with_header(
949 "Content-Type: application/json".parse::<tiny_http::Header>().unwrap()
950 )
951 );
952 continue;
953 }
954
955 let parsed: CuActionRequest = match serde_json::from_str(&body) {
956 Ok(r) => r,
957 Err(e) => {
958 let _ = request.respond(tiny_http::Response::from_string(format!(
959 "{{\"error\":\"Invalid JSON: {}\"}}", e
960 ))
961 .with_status_code(400)
962 .with_header(
963 "Content-Type: application/json".parse::<tiny_http::Header>().unwrap()
964 ));
965 continue;
966 }
967 };
968
969 let json_response = match resolve_backend() {
970 Ok(backend) => {
971 if backend.name() != last_backend {
972 last_backend = backend.name();
973 println!("[ComputerUse] Using {} backend", last_backend);
974 }
975 handle_action(parsed, backend.as_ref())
976 }
977 Err(e) => format!("{{\"error\":\"{}\"}}", e.replace('"', "\\\"")),
978 };
979 let _ = request.respond(
980 tiny_http::Response::from_string(json_response)
981 .with_status_code(200)
982 .with_header(
983 "Content-Type: application/json".parse::<tiny_http::Header>().unwrap()
984 )
985 );
986 }
987}