Skip to main content

rebind_client/
client.rs

1use std::collections::HashMap;
2use std::sync::atomic::{AtomicU64, Ordering};
3use std::sync::Arc;
4use std::time::Duration;
5
6use futures_util::{SinkExt, StreamExt};
7use serde_json::{json, Value};
8use tokio::sync::{mpsc, oneshot, Mutex};
9use tokio::time::timeout;
10use tokio_tungstenite::{connect_async, tungstenite::Message};
11
12use crate::error::{RebindError, Result};
13use crate::types::*;
14
15type PendingMap = Arc<Mutex<HashMap<u64, oneshot::Sender<Value>>>>;
16
17/// Async WebSocket client for the Rebind remote access protocol.
18///
19/// All HID write methods are fire-and-forget (synchronous, queue internally).
20/// All read methods are async and return typed results.
21///
22/// # Example
23///
24/// ```no_run
25/// use rebind_client::RebindClient;
26///
27/// #[tokio::main]
28/// async fn main() -> rebind_client::Result<()> {
29///     let mut client = RebindClient::connect("ws://127.0.0.1:19561").await?;
30///     client.hid_move(30, -5);
31///     let (x, y) = client.system_mouse().await?;
32///     println!("{x} {y}");
33///     client.close().await;
34///     Ok(())
35/// }
36/// ```
37pub struct RebindClient {
38    sender: mpsc::UnboundedSender<Message>,
39    pending: PendingMap,
40    next_id: Arc<AtomicU64>,
41    timeout_ms: u64,
42    // event stream senders keyed by event name
43    event_senders: Arc<Mutex<HashMap<String, mpsc::UnboundedSender<Value>>>>,
44    // background task handle — kept alive until close()
45    _task: tokio::task::JoinHandle<()>,
46}
47
48impl RebindClient {
49    /// Connect to a Rebind relay. Authenticates if `token` is non-empty.
50    pub async fn connect(url: &str) -> Result<Self> {
51        Self::connect_with_options(url, "", 5000).await
52    }
53
54    pub async fn connect_with_token(url: &str, token: &str) -> Result<Self> {
55        Self::connect_with_options(url, token, 5000).await
56    }
57
58    pub async fn connect_with_options(url: &str, token: &str, timeout_ms: u64) -> Result<Self> {
59        let (ws_stream, _) = connect_async(url).await?;
60        let (mut write, mut read) = ws_stream.split();
61
62        let pending: PendingMap = Arc::new(Mutex::new(HashMap::new()));
63        let event_senders: Arc<Mutex<HashMap<String, mpsc::UnboundedSender<Value>>>> =
64            Arc::new(Mutex::new(HashMap::new()));
65        let next_id = Arc::new(AtomicU64::new(1));
66
67        // read the hello banner
68        let banner = match read.next().await {
69            Some(Ok(Message::Text(raw))) => serde_json::from_str::<Value>(&raw)?,
70            _ => return Err(RebindError::connection("no hello banner received")),
71        };
72        if banner.get("t").and_then(|v| v.as_str()) != Some("hello") {
73            return Err(RebindError::connection("unexpected banner"));
74        }
75
76        let (tx, mut rx) = mpsc::unbounded_channel::<Message>();
77
78        // writer task
79        let writer_task = {
80            tokio::spawn(async move {
81                while let Some(msg) = rx.recv().await {
82                    if write.send(msg).await.is_err() {
83                        break;
84                    }
85                }
86            })
87        };
88
89        let pending_reader = pending.clone();
90        let event_senders_reader = event_senders.clone();
91
92        // reader task
93        let reader_task = tokio::spawn(async move {
94            while let Some(Ok(frame)) = read.next().await {
95                let Message::Text(raw) = frame else {
96                    continue;
97                };
98                let Ok(msg) = serde_json::from_str::<Value>(&raw) else {
99                    continue;
100                };
101                if let Some(id) = msg.get("id").and_then(|v| v.as_u64()) {
102                    let mut map = pending_reader.lock().await;
103                    if let Some(tx) = map.remove(&id) {
104                        let _ = tx.send(msg);
105                    }
106                } else if let Some(t) = msg.get("t").and_then(|v| v.as_str()) {
107                    let senders = event_senders_reader.lock().await;
108                    if let Some(sender) = senders.get(t) {
109                        let _ = sender.send(msg);
110                    }
111                }
112            }
113            // stop the writer first so its receiver drops and later RPCs see a
114            // closed sender, then fail in-flight RPCs and end every event
115            // receiver by dropping their senders
116            writer_task.abort();
117            let _ = writer_task.await;
118            pending_reader.lock().await.clear();
119            event_senders_reader.lock().await.clear();
120        });
121
122        let client = Self {
123            sender: tx.clone(),
124            pending,
125            next_id,
126            timeout_ms,
127            event_senders,
128            _task: reader_task,
129        };
130
131        // authenticate if token provided
132        if !token.is_empty() {
133            let result = client
134                .rpc(json!({ "t": "auth", "token": token }))
135                .await?;
136            if result.get("ok").and_then(|v| v.as_bool()) != Some(true) {
137                return Err(RebindError::server("bad_token", "server rejected token"));
138            }
139        }
140
141        Ok(client)
142    }
143
144    /// Close the connection gracefully.
145    pub async fn close(self) {
146        let _ = self.sender.send(Message::Close(None));
147        self._task.abort();
148    }
149
150    // ── HID writes (fire-and-forget) ──────────────────────────────────────
151
152    pub fn hid_down(&self, code: &str) {
153        self.one_shot(json!({ "t": "hid.down", "code": code }));
154    }
155
156    pub fn hid_up(&self, code: &str) {
157        self.one_shot(json!({ "t": "hid.up", "code": code }));
158    }
159
160    pub fn hid_press(&self, code: &str, hold_ms: u32) {
161        self.one_shot(json!({ "t": "hid.press", "code": code, "hold_ms": hold_ms }));
162    }
163
164    pub fn hid_type(&self, text: &str) {
165        self.one_shot(json!({ "t": "hid.type", "text": text }));
166    }
167
168    pub fn hid_move(&self, dx: i32, dy: i32) {
169        self.one_shot(json!({ "t": "hid.move", "dx": dx, "dy": dy }));
170    }
171
172    pub fn hid_move_to(&self, x: i32, y: i32) {
173        self.one_shot(json!({ "t": "hid.move_to", "x": x, "y": y }));
174    }
175
176    pub fn hid_scroll(&self, delta: i32) {
177        self.one_shot(json!({ "t": "hid.scroll", "delta": delta }));
178    }
179
180    // ── reads ─────────────────────────────────────────────────────────────
181
182    pub async fn screen_pixel(&self, x: i32, y: i32) -> Result<Pixel> {
183        let r = self.rpc(json!({ "t": "screen.pixel", "x": x, "y": y })).await?;
184        Ok(Pixel {
185            r: r["r"].as_u64().unwrap_or(0) as u8,
186            g: r["g"].as_u64().unwrap_or(0) as u8,
187            b: r["b"].as_u64().unwrap_or(0) as u8,
188        })
189    }
190
191    pub async fn screen_resolution(&self) -> Result<Resolution> {
192        let r = self.rpc(json!({ "t": "screen.resolution" })).await?;
193        Ok(Resolution {
194            width: r["width"].as_u64().unwrap_or(0) as u32,
195            height: r["height"].as_u64().unwrap_or(0) as u32,
196        })
197    }
198
199    pub async fn system_mouse(&self) -> Result<(i32, i32)> {
200        let r = self.rpc(json!({ "t": "system.mouse" })).await?;
201        Ok((
202            r["x"].as_i64().unwrap_or(0) as i32,
203            r["y"].as_i64().unwrap_or(0) as i32,
204        ))
205    }
206
207    pub async fn system_window(&self) -> Result<WindowInfo> {
208        let r = self.rpc(json!({ "t": "system.window" })).await?;
209        let w = &r["window"];
210        Ok(WindowInfo {
211            title: w["title"].as_str().unwrap_or("").to_string(),
212            process: w["process"].as_str().unwrap_or("").to_string(),
213            x: w["x"].as_i64().unwrap_or(0) as i32,
214            y: w["y"].as_i64().unwrap_or(0) as i32,
215            width: w["width"].as_i64().unwrap_or(0) as i32,
216            height: w["height"].as_i64().unwrap_or(0) as i32,
217        })
218    }
219
220    pub async fn system_time(&self) -> Result<u64> {
221        let r = self.rpc(json!({ "t": "system.time" })).await?;
222        Ok(r["time_ms"].as_u64().unwrap_or(0))
223    }
224
225    pub async fn input_keys(&self) -> Result<Vec<String>> {
226        let r = self.rpc(json!({ "t": "input.keys" })).await?;
227        Ok(r["keys"]
228            .as_array()
229            .unwrap_or(&vec![])
230            .iter()
231            .filter_map(|v| v.as_str().map(str::to_string))
232            .collect())
233    }
234
235    pub async fn input_is_down(&self, code: &str) -> Result<bool> {
236        let r = self.rpc(json!({ "t": "input.is_down", "code": code })).await?;
237        Ok(r["down"].as_bool().unwrap_or(false))
238    }
239
240    pub async fn input_modifiers(&self) -> Result<Modifiers> {
241        let r = self.rpc(json!({ "t": "input.modifiers" })).await?;
242        let m = &r["modifiers"];
243        Ok(Modifiers {
244            shift: m["shift"].as_bool().unwrap_or(false),
245            ctrl: m["ctrl"].as_bool().unwrap_or(false),
246            alt: m["alt"].as_bool().unwrap_or(false),
247            win: m["win"].as_bool().unwrap_or(false),
248        })
249    }
250
251    pub async fn clipboard_get(&self) -> Result<String> {
252        let r = self.rpc(json!({ "t": "clipboard.get" })).await?;
253        Ok(r["text"].as_str().unwrap_or("").to_string())
254    }
255
256    pub async fn clipboard_set(&self, text: &str) -> Result<()> {
257        self.rpc(json!({ "t": "clipboard.set", "text": text })).await?;
258        Ok(())
259    }
260
261    pub async fn window_list(&self, filter: Option<&str>) -> Result<Vec<WindowInfo>> {
262        let req = match filter {
263            Some(f) => json!({ "t": "window.list", "filter": f }),
264            None => json!({ "t": "window.list" }),
265        };
266        let r = self.rpc(req).await?;
267        let windows = r["windows"]
268            .as_array()
269            .unwrap_or(&vec![])
270            .iter()
271            .map(|w| WindowInfo {
272                title: w["title"].as_str().unwrap_or("").to_string(),
273                process: w["process"].as_str().unwrap_or("").to_string(),
274                x: w["x"].as_i64().unwrap_or(0) as i32,
275                y: w["y"].as_i64().unwrap_or(0) as i32,
276                width: w["width"].as_i64().unwrap_or(0) as i32,
277                height: w["height"].as_i64().unwrap_or(0) as i32,
278            })
279            .collect();
280        Ok(windows)
281    }
282
283    pub async fn window_find(&self, title: &str) -> Result<Option<i64>> {
284        let r = self.rpc(json!({ "t": "window.find", "title": title })).await?;
285        Ok(r["handle"].as_i64())
286    }
287
288    pub async fn window_activate(&self, handle: i64) -> Result<()> {
289        self.rpc(json!({ "t": "window.activate", "handle": handle })).await?;
290        Ok(())
291    }
292
293    pub async fn ping(&self) -> Result<u64> {
294        let r = self.rpc(json!({ "t": "ping" })).await?;
295        Ok(r["time_ms"].as_u64().unwrap_or(0))
296    }
297
298    /// Send any server command and return its reply (without the id), including commands
299    /// without a typed method. `args` must be a JSON object; its `t` and `id`
300    /// are replaced by `command` and the correlation id.
301    ///
302    /// ```no_run
303    /// # async fn run(client: &rebind_client::RebindClient) -> rebind_client::Result<()> {
304    /// let r = client.call("hash.sha256", serde_json::json!({ "data": "hello" })).await?;
305    /// println!("{}", r["digest"]);
306    /// # Ok(())
307    /// # }
308    /// ```
309    pub async fn call(&self, command: &str, mut args: Value) -> Result<Value> {
310        let Some(obj) = args.as_object_mut() else {
311            return Err(RebindError::Json(serde::de::Error::custom(
312                "call args must be a JSON object",
313            )));
314        };
315        obj.insert("t".to_string(), json!(command));
316        let mut reply = self.rpc(args).await?;
317        if let Some(obj) = reply.as_object_mut() {
318            obj.remove("id");
319        }
320        Ok(reply)
321    }
322
323    // ── event streams ─────────────────────────────────────────────────────
324
325    /// Subscribe to mouse position events. Returns a receiver that yields
326    /// `Point` values until the connection closes. Dropping the receiver
327    /// stops delivery locally; the server keeps sending until disconnect.
328    pub async fn mouse_events(&self) -> Result<mpsc::UnboundedReceiver<Point>> {
329        let raw_rx = self.subscribe_raw("mouse").await?;
330        let (tx, rx) = mpsc::unbounded_channel();
331        tokio::spawn(async move {
332            let mut raw = raw_rx;
333            while let Some(v) = raw.recv().await {
334                let x = v["x"].as_i64().unwrap_or(0) as i32;
335                let y = v["y"].as_i64().unwrap_or(0) as i32;
336                if tx.send(Point { x, y }).is_err() {
337                    break;
338                }
339            }
340        });
341        Ok(rx)
342    }
343
344    /// Subscribe to window focus change events.
345    pub async fn window_events(&self) -> Result<mpsc::UnboundedReceiver<WindowInfo>> {
346        let raw_rx = self.subscribe_raw("window").await?;
347        let (tx, rx) = mpsc::unbounded_channel();
348        tokio::spawn(async move {
349            let mut raw = raw_rx;
350            while let Some(v) = raw.recv().await {
351                let w = &v["window"];
352                let info = WindowInfo {
353                    title: w["title"].as_str().unwrap_or("").to_string(),
354                    process: w["process"].as_str().unwrap_or("").to_string(),
355                    x: w["x"].as_i64().unwrap_or(0) as i32,
356                    y: w["y"].as_i64().unwrap_or(0) as i32,
357                    width: w["width"].as_i64().unwrap_or(0) as i32,
358                    height: w["height"].as_i64().unwrap_or(0) as i32,
359                };
360                if tx.send(info).is_err() {
361                    break;
362                }
363            }
364        });
365        Ok(rx)
366    }
367
368    // ── internals ─────────────────────────────────────────────────────────
369
370    fn one_shot(&self, mut msg: Value) {
371        // fire-and-forget: no id field
372        if let Some(obj) = msg.as_object_mut() {
373            obj.remove("id");
374        }
375        let text = serde_json::to_string(&msg).unwrap_or_default();
376        let _ = self.sender.send(Message::Text(text.into()));
377    }
378
379    async fn rpc(&self, mut msg: Value) -> Result<Value> {
380        if self.sender.is_closed() {
381            return Err(RebindError::connection("not connected"));
382        }
383
384        let id = self.next_id.fetch_add(1, Ordering::Relaxed);
385        if let Some(obj) = msg.as_object_mut() {
386            obj.insert("id".to_string(), json!(id));
387        }
388
389        let (tx, rx) = oneshot::channel();
390        self.pending.lock().await.insert(id, tx);
391
392        let text = serde_json::to_string(&msg)?;
393        if self.sender.send(Message::Text(text.into())).is_err() {
394            self.pending.lock().await.remove(&id);
395            return Err(RebindError::connection("not connected"));
396        }
397
398        let resp = match timeout(Duration::from_millis(self.timeout_ms), rx).await {
399            Ok(reply) => reply
400                .map_err(|_| RebindError::connection("connection closed while waiting for RPC"))?,
401            Err(_) => {
402                self.pending.lock().await.remove(&id);
403                return Err(RebindError::timeout(
404                    msg.get("t")
405                        .and_then(|v| v.as_str())
406                        .unwrap_or("unknown")
407                        .to_string(),
408                ));
409            }
410        };
411
412        if let Some(err) = resp.get("error") {
413            let code = err["code"].as_str().unwrap_or("unknown");
414            let message = err["message"].as_str().unwrap_or("");
415            return Err(RebindError::server(code, message));
416        }
417
418        Ok(resp)
419    }
420
421    async fn subscribe_raw(&self, event: &str) -> Result<mpsc::UnboundedReceiver<Value>> {
422        let (tx, rx) = mpsc::unbounded_channel();
423        self.event_senders.lock().await.insert(event.to_string(), tx);
424        self.rpc(json!({ "t": "subscribe", "events": [event] })).await?;
425        Ok(rx)
426    }
427}