1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
//! WebAssembly bindings for the SPICE client
//!
//! This module provides the JavaScript API for the SPICE client when compiled to WebAssembly.
use crate::{SpiceClientShared, SpiceError};
use std::sync::Arc;
use tokio::sync::Mutex;
use wasm_bindgen::prelude::*;
use web_sys::{console, HtmlCanvasElement};
/// SPICE client for WebAssembly
///
/// This is the main entry point for using the SPICE client in a web browser.
#[wasm_bindgen]
pub struct SpiceClient {
inner: Arc<Mutex<Option<SpiceClientShared>>>,
websocket_url: String,
canvas: Option<HtmlCanvasElement>,
password: Option<String>,
}
#[wasm_bindgen]
impl SpiceClient {
/// Create a new SPICE client instance
///
/// # Arguments
/// * `websocket_url` - The WebSocket URL to connect to (e.g., "ws://localhost:5959")
/// * `canvas` - The HTML canvas element to render the display
#[wasm_bindgen(constructor)]
pub fn new(websocket_url: String, canvas: HtmlCanvasElement) -> Self {
// Set up console error panic hook for better error messages
console_error_panic_hook::set_once();
console::log_1(&format!("Creating new SPICE client for {websocket_url}").into());
Self {
inner: Arc::new(Mutex::new(None)),
websocket_url,
canvas: Some(canvas),
password: None,
}
}
/// Create a new SPICE client with password authentication
///
/// # Arguments
/// * `websocket_url` - The WebSocket URL to connect to
/// * `canvas` - The HTML canvas element to render the display
/// * `password` - The SPICE password for authentication
#[wasm_bindgen(js_name = "newWithPassword")]
pub fn new_with_password(
websocket_url: String,
canvas: HtmlCanvasElement,
password: String,
) -> Self {
console_error_panic_hook::set_once();
console::log_1(
&format!(
"Creating new SPICE client for {} with password",
websocket_url
)
.into(),
);
Self {
inner: Arc::new(Mutex::new(None)),
websocket_url,
canvas: Some(canvas),
password: Some(password),
}
}
/// Connect to the SPICE server
#[wasm_bindgen]
pub async fn connect(&mut self) -> Result<(), JsValue> {
console::log_1(&format!("Connecting to SPICE server at {}...", self.websocket_url).into());
// Check if already connected
if self.inner.lock().await.is_some() {
console::log_1(&"Already connected".into());
return Ok(());
}
let mut client = SpiceClientShared::new_websocket(self.websocket_url.clone());
// Set canvas for rendering if available
if let Some(canvas) = self.canvas.take() {
console::log_1(&"Setting canvas for rendering".into());
client.set_canvas(canvas.clone()).await;
self.canvas = Some(canvas); // Put it back
}
// Set password if provided
if let Some(ref password) = self.password {
client.set_password(password.clone()).await;
}
match client.connect().await {
Ok(()) => {
console::log_1(&"Connected successfully".into());
// Start the event loop
match client.start_event_loop().await {
Ok(()) => {
console::log_1(&"Event loop started".into());
*self.inner.lock().await = Some(client);
Ok(())
}
Err(e) => {
let error_msg = format!("Failed to start event loop: {e}");
console::error_1(&error_msg.clone().into());
Err(JsValue::from_str(&error_msg))
}
}
}
Err(e) => {
let error_msg = format!("Connection failed: {e}");
console::error_1(&error_msg.clone().into());
Err(JsValue::from_str(&error_msg))
}
}
}
/// Disconnect from the SPICE server
#[wasm_bindgen]
pub async fn disconnect(&mut self) {
console::log_1(&"Disconnecting from SPICE server...".into());
if let Some(client) = self.inner.lock().await.take() {
client.disconnect().await;
console::log_1(&"Disconnected".into());
}
}
/// Get any error state from the client
#[wasm_bindgen]
pub async fn get_error(&self) -> Result<Option<String>, JsValue> {
if let Some(client) = self.inner.lock().await.as_ref() {
Ok(client.get_error_state().await)
} else {
Ok(None)
}
}
/// Send a key event to the server
///
/// This method spawns the actual work to avoid blocking and prevent recursive use errors
#[wasm_bindgen]
pub fn send_key(&self, key_code: u32, is_pressed: bool) -> Result<(), JsValue> {
let inner = self.inner.clone();
wasm_bindgen_futures::spawn_local(async move {
if let Some(client) = inner.lock().await.as_ref() {
// Check for error state first
if let Some(error) = client.get_error_state().await {
console::error_1(
&format!("Cannot send key event - client in error state: {error}").into(),
);
return;
}
// TODO: Implement key event sending through the input channel
console::log_2(
&format!("Key event: code={key_code}, pressed={is_pressed}").into(),
&JsValue::NULL,
);
}
});
Ok(())
}
/// Send a mouse move event to the server
///
/// This method spawns the actual work to avoid blocking and prevent recursive use errors
#[wasm_bindgen]
pub fn send_mouse_move(&self, x: i32, y: i32) -> Result<(), JsValue> {
let inner = self.inner.clone();
wasm_bindgen_futures::spawn_local(async move {
if let Some(client) = inner.lock().await.as_ref() {
// Check for error state first
if let Some(_error) = client.get_error_state().await {
// Silently ignore mouse moves in error state to avoid log spam
return;
}
// TODO: Implement mouse move event sending through the input channel
console::log_2(&format!("Mouse move: x={x}, y={y}").into(), &JsValue::NULL);
}
});
Ok(())
}
/// Send a mouse button event to the server
///
/// This method spawns the actual work to avoid blocking and prevent recursive use errors
#[wasm_bindgen]
pub fn send_mouse_button(&self, button: u8, is_pressed: bool) -> Result<(), JsValue> {
let inner = self.inner.clone();
wasm_bindgen_futures::spawn_local(async move {
if let Some(client) = inner.lock().await.as_ref() {
// Check for error state first
if let Some(error) = client.get_error_state().await {
console::error_1(
&format!(
"Cannot send mouse button event - client in error state: {}",
error
)
.into(),
);
return;
}
// TODO: Implement mouse button event sending through the input channel
console::log_2(
&format!("Mouse button: button={button}, pressed={is_pressed}").into(),
&JsValue::NULL,
);
}
});
Ok(())
}
}
/// Initialize the WASM module
///
/// This should be called once when the module is loaded.
#[wasm_bindgen(start)]
pub fn main() {
// Set up better panic messages in the browser console
console_error_panic_hook::set_once();
// Initialize logging
tracing_wasm::set_as_global_default();
console::log_1(&"SPICE client WASM module initialized".into());
}