@pipelab/asset-tauri 1.0.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/LICENSE +110 -0
  2. package/package.json +18 -0
  3. package/template/README.md +7 -0
  4. package/template/index.html +45 -0
  5. package/template/package.json +23 -0
  6. package/template/pnpm-lock.yaml +715 -0
  7. package/template/src-tauri/Cargo.lock +11191 -0
  8. package/template/src-tauri/Cargo.toml +40 -0
  9. package/template/src-tauri/build.rs +3 -0
  10. package/template/src-tauri/capabilities/default.json +7 -0
  11. package/template/src-tauri/icons/128x128.png +0 -0
  12. package/template/src-tauri/icons/128x128@2x.png +0 -0
  13. package/template/src-tauri/icons/32x32.png +0 -0
  14. package/template/src-tauri/icons/Square107x107Logo.png +0 -0
  15. package/template/src-tauri/icons/Square142x142Logo.png +0 -0
  16. package/template/src-tauri/icons/Square150x150Logo.png +0 -0
  17. package/template/src-tauri/icons/Square284x284Logo.png +0 -0
  18. package/template/src-tauri/icons/Square30x30Logo.png +0 -0
  19. package/template/src-tauri/icons/Square310x310Logo.png +0 -0
  20. package/template/src-tauri/icons/Square44x44Logo.png +0 -0
  21. package/template/src-tauri/icons/Square71x71Logo.png +0 -0
  22. package/template/src-tauri/icons/Square89x89Logo.png +0 -0
  23. package/template/src-tauri/icons/StoreLogo.png +0 -0
  24. package/template/src-tauri/icons/icon.icns +0 -0
  25. package/template/src-tauri/icons/icon.ico +0 -0
  26. package/template/src-tauri/icons/icon.png +0 -0
  27. package/template/src-tauri/src/lib copy 2.rs +438 -0
  28. package/template/src-tauri/src/lib copy.rs +449 -0
  29. package/template/src-tauri/src/lib.rs +616 -0
  30. package/template/src-tauri/src/lib_webview_screenshot.rs +430 -0
  31. package/template/src-tauri/src/main.rs +6 -0
  32. package/template/src-tauri/tauri.conf.json +36 -0
  33. package/template/tsconfig.json +23 -0
@@ -0,0 +1,616 @@
1
+ use futures_util::{
2
+ stream::{SplitSink, SplitStream},
3
+ SinkExt, StreamExt,
4
+ };
5
+ use serde::{Deserialize, Serialize};
6
+ use serde_json::Value; // Using Value for flexibility in body initially
7
+ use std::{net::SocketAddr, sync::Arc};
8
+ use tauri::{
9
+ async_runtime, webview::WebviewWindowBuilder, AppHandle, Manager, Runtime, WebviewUrl,
10
+ };
11
+ use tokio::{
12
+ net::{TcpListener, TcpStream},
13
+ sync::Mutex, // Using Mutex for the writer part
14
+ };
15
+ use tokio_tungstenite::{accept_async, tungstenite::Message, WebSocketStream};
16
+ // Note: Removed `use anyhow::{Error};` as it's unused when using anyhow::Result<()>
17
+
18
+ // --- Message Structures ---
19
+
20
+ /// Generic structure for incoming WebSocket messages
21
+ #[derive(Deserialize, Debug)]
22
+ struct IncomingMessage {
23
+ url: String,
24
+ #[serde(rename = "correlationId")] // Match JS naming
25
+ correlation_id: Option<String>, // Optional correlation ID
26
+ body: Option<Value>, // Use Option<Value> to handle cases where body might be missing
27
+ }
28
+
29
+ /// Generic structure for outgoing WebSocket responses
30
+ #[derive(Serialize, Debug)]
31
+ struct ResponseMessage<T: Serialize> {
32
+ url: String,
33
+ #[serde(rename = "correlationId")]
34
+ correlation_id: Option<String>, // Echo back the correlation ID
35
+ body: T, // Generic body for success or error
36
+ }
37
+
38
+ /// Example structure for a success response body
39
+ #[derive(Serialize, Debug)]
40
+ struct SuccessBody<T: Serialize> {
41
+ success: bool,
42
+ #[serde(skip_serializing_if = "Option::is_none")] // Don't serialize data if it's None
43
+ data: Option<T>, // Make data optional for responses that don't need it
44
+ }
45
+
46
+ /// Example structure for an error response body
47
+ #[derive(Serialize, Debug)]
48
+ struct ErrorBody {
49
+ success: bool,
50
+ error: String,
51
+ }
52
+
53
+ // --- WebSocket Handling ---
54
+
55
+ /// Handles an individual WebSocket connection
56
+ async fn handle_websocket<R: Runtime>(
57
+ stream: TcpStream,
58
+ app_handle: AppHandle<R>, // Pass AppHandle for Tauri interaction
59
+ ) {
60
+ let addr = stream
61
+ .peer_addr()
62
+ .expect("Connected stream should have peer address");
63
+ println!("New WebSocket connection from: {}", addr);
64
+
65
+ match accept_async(stream).await {
66
+ Ok(ws_stream) => {
67
+ println!("WebSocket connection established: {}", addr);
68
+ let (write, read) = ws_stream.split();
69
+ let writer = Arc::new(Mutex::new(write));
70
+ process_messages(read, writer.clone(), app_handle, addr).await;
71
+ println!("WebSocket connection closed: {}", addr);
72
+ }
73
+ Err(e) => {
74
+ eprintln!("Error during WebSocket handshake for {}: {}", addr, e);
75
+ }
76
+ }
77
+ }
78
+
79
+ /// Processes messages received from a single client
80
+ async fn process_messages<R: Runtime>(
81
+ mut read: SplitStream<WebSocketStream<TcpStream>>,
82
+ writer: Arc<Mutex<SplitSink<WebSocketStream<TcpStream>, Message>>>,
83
+ app_handle: AppHandle<R>,
84
+ addr: SocketAddr,
85
+ ) {
86
+ while let Some(message_result) = read.next().await {
87
+ match message_result {
88
+ Ok(msg) => {
89
+ match msg {
90
+ Message::Text(text) => {
91
+ println!("Received text from {}: {}", addr, text);
92
+ match serde_json::from_str::<IncomingMessage>(&text) {
93
+ Ok(parsed_message) => {
94
+ let writer_clone = writer.clone();
95
+ let app_handle_clone = app_handle.clone();
96
+ let url = parsed_message.url.clone();
97
+ let correlation_id = parsed_message.correlation_id.clone();
98
+
99
+ tokio::spawn(async move {
100
+ let writer_for_route = writer_clone.clone();
101
+ // Using anyhow::Result allows easy error propagation with `?`
102
+ if let Err(e) = route_message(
103
+ parsed_message,
104
+ writer_for_route,
105
+ app_handle_clone,
106
+ )
107
+ .await
108
+ {
109
+ eprintln!(
110
+ "Error handling message for url '{}': {}",
111
+ url, e
112
+ );
113
+ let error_response = ResponseMessage {
114
+ url,
115
+ correlation_id,
116
+ body: ErrorBody {
117
+ success: false,
118
+ error: e.to_string(),
119
+ },
120
+ };
121
+ if let Ok(json_response) =
122
+ serde_json::to_string(&error_response)
123
+ {
124
+ let mut w = writer_clone.lock().await;
125
+ if let Err(send_err) =
126
+ w.send(Message::Text(json_response)).await
127
+ {
128
+ eprintln!(
129
+ "Failed to send error response: {}",
130
+ send_err
131
+ );
132
+ }
133
+ }
134
+ }
135
+ });
136
+ }
137
+ Err(e) => {
138
+ eprintln!(
139
+ "Failed to parse JSON from {}: {}. Message: {}",
140
+ addr, e, text
141
+ );
142
+ let response = ResponseMessage {
143
+ url: "unknown".to_string(),
144
+ correlation_id: None,
145
+ body: ErrorBody {
146
+ success: false,
147
+ error: format!("Invalid JSON format: {}", e),
148
+ },
149
+ };
150
+ if let Ok(json_response) = serde_json::to_string(&response) {
151
+ let mut w = writer.lock().await;
152
+ if let Err(send_err) =
153
+ w.send(Message::Text(json_response)).await
154
+ {
155
+ eprintln!(
156
+ "Failed to send parse error response: {}",
157
+ send_err
158
+ );
159
+ }
160
+ }
161
+ }
162
+ }
163
+ }
164
+ Message::Binary(_) => println!("Received binary data from {} (ignored)", addr),
165
+ Message::Ping(ping_data) => {
166
+ println!("Received Ping from {}", addr);
167
+ let mut w = writer.lock().await;
168
+ if let Err(e) = w.send(Message::Pong(ping_data)).await {
169
+ eprintln!("Failed to send Pong: {}", e);
170
+ }
171
+ }
172
+ Message::Pong(_) => println!("Received Pong from {}", addr),
173
+ Message::Close(_) => {
174
+ println!("Received Close frame from {}", addr);
175
+ break;
176
+ }
177
+ Message::Frame(_) => println!("Received raw Frame from {} (ignored)", addr),
178
+ }
179
+ }
180
+ Err(e) => {
181
+ eprintln!("WebSocket error reading message from {}: {}", addr, e);
182
+ break;
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ /// Routes the parsed message to the appropriate handler
189
+ // --- Fix: Changed return type to anyhow::Result<()> ---
190
+ async fn route_message<R: Runtime>(
191
+ message: IncomingMessage,
192
+ writer: Arc<Mutex<SplitSink<WebSocketStream<TcpStream>, Message>>>,
193
+ app_handle: AppHandle<R>,
194
+ ) -> anyhow::Result<()> {
195
+ // Use anyhow::Result for easier error handling
196
+ println!("Routing message for URL: {}", message.url);
197
+
198
+ match message.url.as_str() {
199
+ "/paths" => handle_paths(message, writer, app_handle).await?,
200
+ "/fs/file/write" => handle_fs_write(message, writer, app_handle).await?,
201
+ "/window/maximize" => handle_window_maximize(message, writer, app_handle).await?,
202
+ "/fs/file/read" => handle_not_implemented(message, writer).await?,
203
+ "/fs/file/read/binary" => handle_not_implemented(message, writer).await?,
204
+ "/fs/folder/create" => handle_not_implemented(message, writer).await?,
205
+ "/window/minimize" => handle_window_minimize(message, writer, app_handle).await?,
206
+ "/window/request-attention" => handle_not_implemented(message, writer).await?,
207
+ "/window/restore" => handle_window_restore(message, writer, app_handle).await?,
208
+ "/dialog/folder" => handle_not_implemented(message, writer).await?,
209
+ "/dialog/open" => handle_not_implemented(message, writer).await?,
210
+ "/dialog/save" => handle_not_implemented(message, writer).await?,
211
+ "/window/set-always-on-top" => handle_not_implemented(message, writer).await?,
212
+ "/window/set-height" => handle_not_implemented(message, writer).await?,
213
+ "/window/set-maximum-size" => handle_not_implemented(message, writer).await?,
214
+ "/window/set-minimum-size" => handle_not_implemented(message, writer).await?,
215
+ "/window/set-resizable" => handle_not_implemented(message, writer).await?,
216
+ "/window/set-title" => handle_not_implemented(message, writer).await?,
217
+ "/window/set-width" => handle_not_implemented(message, writer).await?,
218
+ "/window/set-x" => handle_not_implemented(message, writer).await?,
219
+ "/window/set-y" => handle_not_implemented(message, writer).await?,
220
+ "/window/show-dev-tools" => handle_not_implemented(message, writer).await?,
221
+ "/window/unmaximize" => handle_window_unmaximize(message, writer, app_handle).await?,
222
+ "/window/set-fullscreen" => handle_not_implemented(message, writer).await?,
223
+ "/engine" => handle_engine(message, writer).await?,
224
+ "/open" => handle_not_implemented(message, writer).await?,
225
+ "/show-in-explorer" => handle_not_implemented(message, writer).await?,
226
+ "/run" => handle_not_implemented(message, writer).await?,
227
+ "/fs/copy" => handle_not_implemented(message, writer).await?,
228
+ "/fs/delete" => handle_not_implemented(message, writer).await?,
229
+ "/fs/exist" => handle_not_implemented(message, writer).await?,
230
+ "/fs/list" => handle_not_implemented(message, writer).await?,
231
+ "/fs/file/size" => handle_not_implemented(message, writer).await?,
232
+ "/fs/move" => handle_not_implemented(message, writer).await?,
233
+ "/steam/raw" => handle_not_implemented(message, writer).await?,
234
+ "/discord/set-activity" => handle_not_implemented(message, writer).await?,
235
+ "/infos" => handle_not_implemented(message, writer).await?,
236
+ "/exit" => handle_exit(message, writer, app_handle).await?,
237
+
238
+ _ => {
239
+ println!("Received unhandled URL: {}", message.url);
240
+ let response = ResponseMessage {
241
+ url: message.url.clone(),
242
+ correlation_id: message.correlation_id.clone(),
243
+ body: ErrorBody {
244
+ success: false,
245
+ error: format!("Unhandled URL: {}", message.url),
246
+ },
247
+ };
248
+ let json_response = serde_json::to_string(&response)?;
249
+ let mut w = writer.lock().await;
250
+ w.send(Message::Text(json_response)).await?;
251
+ }
252
+ }
253
+ Ok(())
254
+ }
255
+
256
+ // --- Example Handler Implementations (Stubs) ---
257
+
258
+ // --- Fix: Changed return type for all handlers to anyhow::Result<()> ---
259
+
260
+ async fn handle_not_implemented(
261
+ message: IncomingMessage,
262
+ writer: Arc<Mutex<SplitSink<WebSocketStream<TcpStream>, Message>>>,
263
+ ) -> anyhow::Result<()> {
264
+ // Use anyhow::Result
265
+ println!("Handler not implemented for URL: {}", message.url);
266
+ let response = ResponseMessage {
267
+ url: message.url.clone(),
268
+ correlation_id: message.correlation_id.clone(),
269
+ body: ErrorBody {
270
+ success: false,
271
+ error: format!("Feature not implemented: {}", message.url),
272
+ },
273
+ };
274
+ let json_response = serde_json::to_string(&response)?;
275
+ let mut w = writer.lock().await;
276
+ w.send(Message::Text(json_response)).await?;
277
+ Ok(())
278
+ }
279
+
280
+ async fn handle_engine(
281
+ message: IncomingMessage,
282
+ writer: Arc<Mutex<SplitSink<WebSocketStream<TcpStream>, Message>>>,
283
+ ) -> anyhow::Result<()> {
284
+ // Use anyhow::Result
285
+ println!(
286
+ "Handling /engine request. Body (if any): {:?}",
287
+ message.body
288
+ );
289
+ let response = ResponseMessage {
290
+ url: message.url.clone(),
291
+ correlation_id: message.correlation_id.clone(),
292
+ body: SuccessBody {
293
+ success: true,
294
+ data: Option::<()>::None,
295
+ },
296
+ };
297
+ let json_response = serde_json::to_string(&response)?;
298
+ let mut w = writer.lock().await;
299
+ w.send(Message::Text(json_response)).await?;
300
+ Ok(())
301
+ }
302
+
303
+ async fn handle_paths<R: Runtime>(
304
+ message: IncomingMessage,
305
+ writer: Arc<Mutex<SplitSink<WebSocketStream<TcpStream>, Message>>>,
306
+ app_handle: AppHandle<R>,
307
+ ) -> anyhow::Result<()> {
308
+ // Use anyhow::Result
309
+ println!("Handling /paths request. Body (if any): {:?}", message.body);
310
+
311
+ // --- Fix: Use ok_or_else correctly on Option ---
312
+ // let user_data_path = app_handle.path().app_data_dir()
313
+ // .ok_or_else(|| anyhow::anyhow!("Could not get app data dir"))?; // ok_or_else is called on Option<PathBuf>
314
+ // let documents_path = dirs::document_dir()
315
+ // .ok_or_else(|| anyhow::anyhow!("Could not get documents dir"))?; // ok_or_else is called on Option<PathBuf>
316
+
317
+ let data = serde_json::json!({
318
+ "appData": "data", // user_data_path,
319
+ "documents": "data", // documents_path,
320
+ });
321
+
322
+ let response = ResponseMessage {
323
+ url: message.url.clone(),
324
+ correlation_id: message.correlation_id.clone(),
325
+ body: SuccessBody {
326
+ success: true,
327
+ data: Some(data),
328
+ },
329
+ };
330
+ let json_response = serde_json::to_string(&response)?;
331
+ let mut w = writer.lock().await;
332
+ w.send(Message::Text(json_response)).await?;
333
+ Ok(())
334
+ }
335
+
336
+ async fn handle_fs_write<R: Runtime>(
337
+ message: IncomingMessage,
338
+ writer: Arc<Mutex<SplitSink<WebSocketStream<TcpStream>, Message>>>,
339
+ _app_handle: AppHandle<R>,
340
+ ) -> anyhow::Result<()> {
341
+ // Use anyhow::Result
342
+ println!("Handling /fs/file/write request.");
343
+
344
+ let body_value = message
345
+ .body
346
+ .ok_or_else(|| anyhow::anyhow!("Missing request body for /fs/file/write"))?;
347
+ let path = body_value
348
+ .get("path")
349
+ .and_then(Value::as_str)
350
+ .ok_or_else(|| anyhow::anyhow!("Missing 'path' field in body"))?;
351
+ let content = body_value
352
+ .get("content")
353
+ .and_then(Value::as_str)
354
+ .ok_or_else(|| anyhow::anyhow!("Missing 'content' field in body"))?;
355
+
356
+ println!(
357
+ "Attempting to write to path: '{}', Content length: {}",
358
+ path,
359
+ content.len()
360
+ );
361
+ // tokio::fs::write(path, content).await?; // Add actual file writing logic here
362
+
363
+ let response = ResponseMessage {
364
+ url: message.url.clone(),
365
+ correlation_id: message.correlation_id.clone(),
366
+ body: SuccessBody {
367
+ success: true,
368
+ data: Option::<()>::None,
369
+ },
370
+ };
371
+ let json_response = serde_json::to_string(&response)?;
372
+ let mut w = writer.lock().await;
373
+ w.send(Message::Text(json_response)).await?;
374
+ Ok(())
375
+ }
376
+
377
+ async fn handle_window_maximize<R: Runtime>(
378
+ message: IncomingMessage,
379
+ writer: Arc<Mutex<SplitSink<WebSocketStream<TcpStream>, Message>>>,
380
+ app_handle: AppHandle<R>,
381
+ ) -> anyhow::Result<()> {
382
+ // Use anyhow::Result
383
+ println!("Handling /window/maximize request");
384
+ match app_handle.get_webview_window("main") {
385
+ Some(window) => {
386
+ #[cfg(desktop)] // This block only compiles on desktop targets
387
+ {
388
+ window.maximize()?; // This call is safe here
389
+ println!("Window 'main' maximized.");
390
+ }
391
+ #[cfg(mobile)] // This block only compiles on mobile targets
392
+ {
393
+ // On mobile, maximize doesn't exist/make sense in the same way.
394
+ println!("Window maximize is not supported on mobile. Returning error.");
395
+ // Return an error indicating the operation is not supported on this platform.
396
+ return Err(anyhow::anyhow!(
397
+ "Window maximize is not supported on this platform"
398
+ ));
399
+ }
400
+
401
+ let response = ResponseMessage {
402
+ url: message.url.clone(),
403
+ correlation_id: message.correlation_id.clone(),
404
+ body: SuccessBody {
405
+ success: true,
406
+ data: Option::<()>::None,
407
+ },
408
+ };
409
+ let json_response = serde_json::to_string(&response)?;
410
+ let mut w = writer.lock().await;
411
+ w.send(Message::Text(json_response)).await?;
412
+ }
413
+ None => {
414
+ return Err(anyhow::anyhow!("Main window not found")); // Return anyhow error
415
+ }
416
+ }
417
+ Ok(())
418
+ }
419
+
420
+ async fn handle_window_minimize<R: Runtime>(
421
+ message: IncomingMessage,
422
+ writer: Arc<Mutex<SplitSink<WebSocketStream<TcpStream>, Message>>>,
423
+ app_handle: AppHandle<R>,
424
+ ) -> anyhow::Result<()> {
425
+ // Use anyhow::Result
426
+ println!("Handling /window/minimize request");
427
+ match app_handle.get_webview_window("main") {
428
+ Some(window) => {
429
+ #[cfg(desktop)]
430
+ {
431
+ window.minimize()?;
432
+ println!("Window 'main' minimized.");
433
+ }
434
+ #[cfg(mobile)]
435
+ {
436
+ println!("Window minimize is not supported on mobile. Returning error.");
437
+ return Err(anyhow::anyhow!(
438
+ "Window minimize is not supported on this platform"
439
+ ));
440
+ }
441
+ let response = ResponseMessage {
442
+ url: message.url.clone(),
443
+ correlation_id: message.correlation_id.clone(),
444
+ body: SuccessBody {
445
+ success: true,
446
+ data: Option::<()>::None,
447
+ },
448
+ };
449
+ let json_response = serde_json::to_string(&response)?;
450
+ let mut w = writer.lock().await;
451
+ w.send(Message::Text(json_response)).await?;
452
+ }
453
+ None => {
454
+ return Err(anyhow::anyhow!("Main window not found"));
455
+ }
456
+ }
457
+ Ok(())
458
+ }
459
+
460
+ async fn handle_window_restore<R: Runtime>(
461
+ message: IncomingMessage,
462
+ writer: Arc<Mutex<SplitSink<WebSocketStream<TcpStream>, Message>>>,
463
+ app_handle: AppHandle<R>,
464
+ ) -> anyhow::Result<()> {
465
+ // Use anyhow::Result
466
+ println!("Handling /window/restore request");
467
+ match app_handle.get_webview_window("main") {
468
+ Some(window) => {
469
+ #[cfg(desktop)]
470
+ {
471
+ if window.is_maximized()? {
472
+ window.unmaximize()?;
473
+ }
474
+ window.set_focus()?;
475
+ if !window.is_visible()? {
476
+ window.show()?;
477
+ }
478
+ println!("Window 'main' restored (attempted).");
479
+ }
480
+ #[cfg(mobile)]
481
+ {
482
+ println!("Window restore is not supported on mobile. Returning error.");
483
+ return Err(anyhow::anyhow!(
484
+ "Window restore is not supported on this platform"
485
+ ));
486
+ }
487
+
488
+ let response = ResponseMessage {
489
+ url: message.url.clone(),
490
+ correlation_id: message.correlation_id.clone(),
491
+ body: SuccessBody {
492
+ success: true,
493
+ data: Option::<()>::None,
494
+ },
495
+ };
496
+ let json_response = serde_json::to_string(&response)?;
497
+ let mut w = writer.lock().await;
498
+ w.send(Message::Text(json_response)).await?;
499
+ }
500
+ None => {
501
+ return Err(anyhow::anyhow!("Main window not found"));
502
+ }
503
+ }
504
+ Ok(())
505
+ }
506
+
507
+ async fn handle_window_unmaximize<R: Runtime>(
508
+ message: IncomingMessage,
509
+ writer: Arc<Mutex<SplitSink<WebSocketStream<TcpStream>, Message>>>,
510
+ app_handle: AppHandle<R>,
511
+ ) -> anyhow::Result<()> {
512
+ // Use anyhow::Result
513
+ println!("Handling /window/unmaximize request");
514
+ match app_handle.get_webview_window("main") {
515
+ Some(window) => {
516
+ #[cfg(desktop)]
517
+ {
518
+ window.unmaximize()?; // <-- Your original line, now conditional
519
+ println!("Window 'main' unmaximized.");
520
+ }
521
+ #[cfg(mobile)]
522
+ {
523
+ println!("Window unmaximize is not supported on mobile. Returning error.");
524
+ return Err(anyhow::anyhow!(
525
+ "Window unmaximize is not supported on this platform"
526
+ ));
527
+ }
528
+ let response = ResponseMessage {
529
+ url: message.url.clone(),
530
+ correlation_id: message.correlation_id.clone(),
531
+ body: SuccessBody {
532
+ success: true,
533
+ data: Option::<()>::None,
534
+ },
535
+ };
536
+ let json_response = serde_json::to_string(&response)?;
537
+ let mut w = writer.lock().await;
538
+ w.send(Message::Text(json_response)).await?;
539
+ }
540
+ None => {
541
+ return Err(anyhow::anyhow!("Main window not found"));
542
+ }
543
+ }
544
+ Ok(())
545
+ }
546
+
547
+ async fn handle_exit<R: Runtime>(
548
+ message: IncomingMessage,
549
+ writer: Arc<Mutex<SplitSink<WebSocketStream<TcpStream>, Message>>>,
550
+ app_handle: AppHandle<R>,
551
+ ) -> anyhow::Result<()> {
552
+ // Use anyhow::Result
553
+ println!("Handling /exit request");
554
+ let response = ResponseMessage {
555
+ url: message.url.clone(),
556
+ correlation_id: message.correlation_id.clone(),
557
+ body: SuccessBody {
558
+ success: true,
559
+ data: Option::<()>::None,
560
+ },
561
+ };
562
+ let json_response = serde_json::to_string(&response)?;
563
+ let mut w = writer.lock().await;
564
+ w.send(Message::Text(json_response)).await?;
565
+
566
+ app_handle.exit(0);
567
+ // TODO: support exit code
568
+ // app_handle.exit(message.body.code);
569
+ Ok(())
570
+ }
571
+
572
+ // --- WebSocket Server ---
573
+
574
+ async fn start_websocket_server<R: Runtime>(app_handle: AppHandle<R>) {
575
+ let addr = SocketAddr::from(([127, 0, 0, 1], 31753));
576
+ let listener = match TcpListener::bind(&addr).await {
577
+ Ok(l) => l,
578
+ Err(e) => {
579
+ eprintln!("Failed to bind WebSocket server to {}: {}", addr, e);
580
+ return;
581
+ }
582
+ };
583
+ println!("WebSocket server running on ws://{}", addr);
584
+
585
+ loop {
586
+ match listener.accept().await {
587
+ Ok((stream, _)) => {
588
+ let app_handle_clone = app_handle.clone();
589
+ tokio::spawn(async move {
590
+ handle_websocket(stream, app_handle_clone).await;
591
+ });
592
+ }
593
+ Err(e) => {
594
+ eprintln!("Failed to accept incoming connection: {}", e);
595
+ tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
596
+ }
597
+ }
598
+ }
599
+ }
600
+
601
+ // --- Tauri Setup ---
602
+
603
+ #[cfg_attr(mobile, tauri::mobile_entry_point)]
604
+ pub fn run() {
605
+ tauri::Builder::default()
606
+ .setup(move |app| {
607
+ let app_handle = app.handle().clone();
608
+ async_runtime::spawn(async move {
609
+ start_websocket_server(app_handle).await;
610
+ });
611
+
612
+ Ok(())
613
+ })
614
+ .run(tauri::generate_context!())
615
+ .expect("error while running tauri application");
616
+ }