refactor: switch from sidecar IPC to webview-based RustDesk integration

Previous approach used Tauri sidecar binary with IPC commands. New approach
uses the RustDesk web architecture correctly:

- RustDesk server runs as native binary in --server mode (ports 21115-21119)
- RustDesk web client (Flutter web build) loaded in a Tauri webview window
- Web client communicates with server via WebSocket (21118/21119)

Backend changes:
- main.rs: Simplified IPC - start/stop server + open webview window
- Removed tokio dependency (no longer needed)
- Removed sidecar bundle config from tauri.conf.json

Frontend changes:
- RemoteDesktop.js: Toggle server on/off, launch web client webview
- New UI with server control section + web client launcher
- Updated CSS for the new layout
- Updated HTML with server URL input and launch button

Documentation:
- Updated binaries/README.md with full build pipeline:
  1. Build RustDesk native binary
  2. Build Flutter web client
  3. WASM bridge dependency note
  4. Server ports reference
This commit is contained in:
Z User 2026-04-06 19:35:50 +00:00
parent f88e2c8fb4
commit 3f0893cdf7
7 changed files with 331 additions and 437 deletions

View File

@ -12,4 +12,3 @@ tauri-build = { version = "2", features = [] }
tauri = { version = "2", features = [] } tauri = { version = "2", features = [] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
tokio = { version = "1", features = ["full"] }

View File

@ -1,29 +1,95 @@
# RustDesk Sidecar Binary # RustDesk Integration — Build Instructions
Place the compiled RustDesk binary here: ## Architecture
- **Windows**: `rustdesk-x86_64-pc-windows-msvc.exe` → rename to `rustdesk.exe` ```
- **Linux**: `rustdesk-x86_64-unknown-linux-gnu` → rename to `rustdesk` Shelled OS (Tauri)
- **macOS**: `rustdesk-universal-apple-darwin` → rename to `rustdesk` ├── Main Window: OS desktop UI (taskbar, popups)
├── RustDesk Server: Native binary (--server mode, ports 21115-21119)
└── RustDesk Web Client: Flutter web build loaded in a Tauri webview
└── Connects to server via WebSocket (ws://localhost:21118, ws://localhost:21119)
```
## How to build RustDesk ## Step 1: Build RustDesk Native Binary
See `shelled/rustdesk-as-ref/CLAUDE.md` for build instructions. The RustDesk binary is built from `shelled/rustdesk-as-ref/`.
### Quick build (Windows with vcpkg) ### Prerequisites (Windows)
- Rust toolchain (`rustup`)
- vcpkg with: `libvpx`, `libyuv`, `opus`, `aom`
- Visual Studio Build Tools (C++ workload)
```powershell ```powershell
cd shelled/rustdesk-as-ref set VCPKG_ROOT=C:\path\to\vcpkg
set VCPKG_ROOT=<your-vcpkg-install-path> cd shelled\rustdesk-as-ref
cargo build --release cargo build --release
``` ```
The output binary will be at: ### Copy binary
`target/release/rustdesk.exe`
Copy it to this directory:
```powershell ```powershell
mkdir shelled\shelled-os-ui\src-tauri\binaries
copy target\release\rustdesk.exe shelled\shelled-os-ui\src-tauri\binaries\rustdesk.exe copy target\release\rustdesk.exe shelled\shelled-os-ui\src-tauri\binaries\rustdesk.exe
``` ```
Tauri will automatically bundle this binary when you run `tauri build`. ## Step 2: Build RustDesk Web Client
The web client is a Flutter web build that communicates with the server via WebSocket.
### Prerequisites
- Flutter SDK 3.24+
- The `flutter/web/js/` WASM bridge (from `rustdesk-web-client` project)
### Build
```powershell
cd shelled\rustdesk-as-ref\flutter
# Install web dependencies
flutter pub get
flutter create --platforms web . # Generate web/ directory if missing
# Build Flutter web
flutter build web --release
```
### Copy to Shelled OS
```powershell
mkdir shelled\shelled-os-ui\rustdesk-web
xcopy build\web\* shelled\shelled-os-ui\rustdesk-web\ /E
```
### Note on the WASM Bridge
The RustDesk web client requires a JS/WASM bridge layer that provides:
- `setByName(name, value)` — Flutter → Rust/WASM commands
- `getByName(name, value)` — Rust/WASM → Flutter data
- `init()` — Initialize the connection
This bridge was removed from the main RustDesk repo. You'll need to get it from:
- GitHub Releases: `web_deps.tar.gz` from `rustdesk/doc.rustdesk.com`
- Or the `MonsieurBiche/rustdesk-web-client` fork's `fix-build` branch
## Step 3: Run Shelled OS
```powershell
cd shelled\shelled-os-ui
npm run dev
```
### Using the Remote Desktop
1. Click the Remote Desktop icon in the taskbar
2. Click "Start Server" to launch the RustDesk server
3. Click "Launch" to open the web client in a new window
4. Connect to remote machines through the web client
## Server Ports
| Port | Protocol | Purpose |
|------|----------|---------|
| 21115 | TCP | NAT type test |
| 21116 | TCP+UDP | ID registration & signaling |
| 21117 | TCP | Relay server |
| 21118 | TCP/WS | WebSocket signaling (web client) |
| 21119 | TCP/WS | WebSocket relay (web client) |
## WebSocket URLs (for web client)
- Signal: `ws://localhost:21118` (or `wss://` behind reverse proxy)
- Relay: `ws://localhost:21119` (or `wss://` behind reverse proxy)

View File

@ -6,12 +6,12 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::process::{Child, Command, Stdio}; use std::process::{Child, Command, Stdio};
use std::sync::Mutex; use std::sync::Mutex;
use tauri::Manager; use tauri::{Manager, WebviewUrl, WebviewWindowBuilder};
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt; use std::os::windows::process::CommandExt;
/// State holding the RustDesk child process /// State holding the RustDesk server process
struct RustDeskState { struct RustDeskState {
process: Mutex<Option<Child>>, process: Mutex<Option<Child>>,
} }
@ -22,23 +22,20 @@ struct RustDeskStatus {
pid: Option<u32>, pid: Option<u32>,
} }
/// Get the path to the RustDesk sidecar binary /// Find the RustDesk binary
fn get_rustdesk_path() -> Result<std::path::PathBuf, String> { fn get_rustdesk_path() -> Result<std::path::PathBuf, String> {
let exe_dir = std::env::current_exe() let exe_dir = std::env::current_exe()
.map_err(|e| format!("Failed to get current exe path: {}", e))?; .map_err(|e| format!("Failed to get current exe path: {}", e))?;
// In development, look alongside the Tauri binary
// In production, look in the same directory (Tauri bundles sidecars there)
let exe_dir = exe_dir let exe_dir = exe_dir
.parent() .parent()
.ok_or("Failed to get parent directory of exe")?; .ok_or("Failed to get parent directory of exe")?;
// Try multiple possible locations
let candidates = vec![ let candidates = vec![
exe_dir.join("rustdesk.exe"), // Bundled sidecar (Windows) exe_dir.join("rustdesk.exe"),
exe_dir.join("binaries").join("rustdesk.exe"), // Explicit binaries dir exe_dir.join("binaries").join("rustdesk.exe"),
exe_dir.join("rustdesk"), // Bundled sidecar (Linux/macOS) exe_dir.join("rustdesk"),
exe_dir.join("binaries").join("rustdesk"), // Explicit binaries dir (Linux/macOS) exe_dir.join("binaries").join("rustdesk"),
]; ];
for candidate in &candidates { for candidate in &candidates {
@ -53,9 +50,10 @@ fn get_rustdesk_path() -> Result<std::path::PathBuf, String> {
)) ))
} }
/// Start the RustDesk sidecar process /// Start the RustDesk server process in background (--server mode)
/// This enables the machine to receive incoming connections
#[tauri::command] #[tauri::command]
async fn start_rustdesk( async fn start_rustdesk_server(
state: tauri::State<'_, RustDeskState>, state: tauri::State<'_, RustDeskState>,
app: tauri::AppHandle, app: tauri::AppHandle,
) -> Result<RustDeskStatus, String> { ) -> Result<RustDeskStatus, String> {
@ -64,10 +62,8 @@ async fn start_rustdesk(
// Already running? // Already running?
if let Some(ref child) = *proc { if let Some(ref child) = *proc {
if let Ok(Some(_)) = child.try_wait() { if let Ok(Some(_)) = child.try_wait() {
// Process exited, clean up
*proc = None; *proc = None;
} else { } else {
// Still running
return Ok(RustDeskStatus { return Ok(RustDeskStatus {
running: true, running: true,
pid: Some(child.id()), pid: Some(child.id()),
@ -77,29 +73,29 @@ async fn start_rustdesk(
let path = get_rustdesk_path()?; let path = get_rustdesk_path()?;
// Start RustDesk in server mode (listens on 21115-21119)
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000; const CREATE_NO_WINDOW: u32 = 0x08000000;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let child = Command::new(&path) let child = Command::new(&path)
.arg("--connect") .arg("--server")
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.creation_flags(CREATE_NO_WINDOW) .creation_flags(CREATE_NO_WINDOW)
.spawn() .spawn()
.map_err(|e| format!("Failed to start RustDesk: {}", e))?; .map_err(|e| format!("Failed to start RustDesk server: {}", e))?;
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
let child = Command::new(&path) let child = Command::new(&path)
.arg("--connect") .arg("--server")
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.spawn() .spawn()
.map_err(|e| format!("Failed to start RustDesk: {}", e))?; .map_err(|e| format!("Failed to start RustDesk server: {}", e))?;
let pid = child.id(); let pid = child.id();
*proc = Some(child); *proc = Some(child);
// Emit event to frontend
app.emit("rustdesk-status", RustDeskStatus { app.emit("rustdesk-status", RustDeskStatus {
running: true, running: true,
pid: Some(pid), pid: Some(pid),
@ -112,9 +108,9 @@ async fn start_rustdesk(
}) })
} }
/// Stop the RustDesk sidecar process /// Stop the RustDesk server process
#[tauri::command] #[tauri::command]
async fn stop_rustdesk( async fn stop_rustdesk_server(
state: tauri::State<'_, RustDeskState>, state: tauri::State<'_, RustDeskState>,
app: tauri::AppHandle, app: tauri::AppHandle,
) -> Result<RustDeskStatus, String> { ) -> Result<RustDeskStatus, String> {
@ -136,7 +132,7 @@ async fn stop_rustdesk(
Ok(status) Ok(status)
} }
/// Get the current status of the RustDesk sidecar /// Get the current server status
#[tauri::command] #[tauri::command]
async fn get_rustdesk_status( async fn get_rustdesk_status(
state: tauri::State<'_, RustDeskState>, state: tauri::State<'_, RustDeskState>,
@ -145,22 +141,18 @@ async fn get_rustdesk_status(
if let Some(ref mut child) = *proc { if let Some(ref mut child) = *proc {
match child.try_wait() { match child.try_wait() {
Ok(Some(_status)) => { Ok(Some(_)) => {
// Process has exited
*proc = None; *proc = None;
Ok(RustDeskStatus { Ok(RustDeskStatus {
running: false, running: false,
pid: None, pid: None,
}) })
} }
Ok(None) => { Ok(None) => Ok(RustDeskStatus {
// Still running running: true,
Ok(RustDeskStatus { pid: Some(child.id()),
running: true, }),
pid: Some(child.id()), Err(e) => Err(format!("Failed to check process: {}", e)),
})
}
Err(e) => Err(format!("Failed to check process status: {}", e)),
} }
} else { } else {
Ok(RustDeskStatus { Ok(RustDeskStatus {
@ -170,63 +162,55 @@ async fn get_rustdesk_status(
} }
} }
/// Connect to a remote peer via RustDesk /// Open the RustDesk web client in a new Tauri webview window.
/// The web client is the Flutter web build embedded in the app.
/// It connects to the local RustDesk server via WebSocket.
#[tauri::command] #[tauri::command]
async fn connect_to_peer( async fn open_rustdesk_web(
state: tauri::State<'_, RustDeskState>,
app: tauri::AppHandle, app: tauri::AppHandle,
peer_id: String, server_url: Option<String>,
) -> Result<RustDeskStatus, String> { ) -> Result<(), String> {
// Stop any existing instance // The RustDesk web client is served from the embedded rustdesk-web/ assets.
{ // In dev: looks at ../rustdesk-web/index.html
let mut proc = state.process.lock().map_err(|e| e.to_string())?; // In prod: bundled as Tauri resource
if let Some(mut child) = proc.take() { let web_path = std::env::current_dir()
let _ = child.kill(); .unwrap_or_default()
let _ = child.wait(); .parent()
.map(|p| p.join("rustdesk-web").join("index.html"))
.filter(|p| p.exists());
let url = if let Some(path) = web_path {
let path_str = path.to_string_lossy().to_string();
// On Windows, convert to file:// URL
#[cfg(target_os = "windows")]
let url = format!("file:///{0}", path_str.replace('\\', "/"));
#[cfg(not(target_os = "windows"))]
let url = format!("file://{0}", path_str);
// Append server URL as query param if provided
if let Some(ref srv) = server_url {
format!("{}?server={}", url, srv)
} else {
url
} }
} } else {
// Fallback: try loading from Tauri asset protocol
let path = get_rustdesk_path()?; // This requires the web client to be bundled via tauri.conf.json resources
"https://web.rustdesk.com".to_string()
#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;
#[cfg(target_os = "windows")]
let child = Command::new(&path)
.arg("--connect")
.arg(&peer_id)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.creation_flags(CREATE_NO_WINDOW)
.spawn()
.map_err(|e| format!("Failed to start RustDesk connection: {}", e))?;
#[cfg(not(target_os = "windows"))]
let child = Command::new(&path)
.arg("--connect")
.arg(&peer_id)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("Failed to start RustDesk connection: {}", e))?;
let pid = child.id();
{
let mut proc = state.process.lock().map_err(|e| e.to_string())?;
*proc = Some(child);
}
let status = RustDeskStatus {
running: true,
pid: Some(pid),
}; };
app.emit("rustdesk-connected", serde_json::json!({ WebviewWindowBuilder::new(
"peer_id": peer_id, &app,
"pid": pid, "rustdesk-web",
})) tauri::WebviewUrl::External(url.parse().map_err(|e| format!("Invalid URL: {}", e))?),
.map_err(|e| e.to_string())?; )
.title("RustDesk - Remote Desktop")
.fullscreen(true)
.inner_size(1280.0, 720.0)
.build()
.map_err(|e| format!("Failed to create webview: {}", e))?;
Ok(status) Ok(())
} }
fn main() { fn main() {
@ -235,10 +219,10 @@ fn main() {
process: Mutex::new(None), process: Mutex::new(None),
}) })
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
start_rustdesk, start_rustdesk_server,
stop_rustdesk, stop_rustdesk_server,
get_rustdesk_status, get_rustdesk_status,
connect_to_peer, open_rustdesk_web,
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");

View File

@ -18,13 +18,5 @@
"security": { "security": {
"csp": null "csp": null
} }
},
"bundle": {
"externalBin": [
"binaries/rustdesk"
],
"resources": [
"binaries/*"
]
} }
} }

View File

@ -1,13 +1,85 @@
/* Remote Desktop Component Styles */ /* Remote Desktop Component Styles */
#remote-desktop { #remote-desktop {
left: 260px; /* Offset from remote desktop icon */ left: 200px;
width: 520px; width: 520px;
height: 560px; height: 500px;
z-index: 95; z-index: 95;
} }
/* Connection Section */ /* Server Control Section */
.rd-server-control {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.rd-server-info {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.rd-server-icon {
font-size: 28px;
color: var(--accent-cyan);
}
.rd-server-details {
display: flex;
flex-direction: column;
}
.rd-server-label {
font-size: 0.95rem;
font-weight: 500;
color: var(--bg-white);
}
.rd-server-ports {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
font-family: monospace;
}
/* Buttons */
.rd-btn-server {
background: linear-gradient(135deg, #11998e, #38ef7d);
white-space: nowrap;
flex-shrink: 0;
}
.rd-btn-server.rd-toggle-active {
background: linear-gradient(135deg, #e52e00, #ff6b35);
}
.rd-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 10px 18px;
border: none;
border-radius: 8px;
font-family: 'Outfit', sans-serif;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s ease, background 0.2s ease, opacity 0.2s ease;
color: white;
}
.rd-btn:hover {
transform: scale(1.03);
}
.rd-btn:active {
transform: scale(0.98);
}
/* Connection/Launch Section */
.rd-connection-section { .rd-connection-section {
padding: 16px 20px; padding: 16px 20px;
border-bottom: 1px solid var(--glass-border); border-bottom: 1px solid var(--glass-border);
@ -22,6 +94,18 @@
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
.rd-launch-section {
display: flex;
flex-direction: column;
gap: 12px;
}
.rd-launch-desc {
font-size: 0.82rem;
color: rgba(255, 255, 255, 0.5);
line-height: 1.4;
}
.rd-input-row { .rd-input-row {
display: flex; display: flex;
gap: 8px; gap: 8px;
@ -50,45 +134,6 @@
color: rgba(255, 255, 255, 0.35); color: rgba(255, 255, 255, 0.35);
} }
.rd-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 10px 18px;
border: none;
border-radius: 8px;
font-family: 'Outfit', sans-serif;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s ease, background 0.2s ease, opacity 0.2s ease;
color: white;
}
.rd-btn:hover {
transform: scale(1.03);
}
.rd-btn:active {
transform: scale(0.98);
}
.rd-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
#rd-connect-btn {
background: linear-gradient(135deg, #0078D7, #00BCF2);
}
#rd-disconnect-btn {
background: linear-gradient(135deg, #e52e00, #ff6b35);
display: none;
}
/* Status Bar */ /* Status Bar */
.rd-status-bar { .rd-status-bar {
display: flex; display: flex;
@ -137,102 +182,6 @@
color: rgba(255, 255, 255, 0.8); color: rgba(255, 255, 255, 0.8);
} }
/* Recent Connections */
.rd-recent-section {
flex-grow: 1;
overflow-y: auto;
padding: 12px 16px;
}
.rd-recent-section h3 {
font-size: 0.85rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.6);
margin-bottom: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.rd-empty-recent {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 32px 20px;
color: rgba(255, 255, 255, 0.3);
font-size: 0.85rem;
}
.rd-empty-recent .material-icons {
font-size: 32px;
}
.rd-recent-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s ease, transform 0.15s ease;
margin-bottom: 4px;
}
.rd-recent-item:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateX(3px);
}
.rd-recent-info {
display: flex;
align-items: center;
gap: 12px;
}
.rd-recent-icon {
font-size: 22px;
color: var(--accent-cyan);
}
.rd-recent-details {
display: flex;
flex-direction: column;
}
.rd-recent-id {
font-size: 0.95rem;
font-weight: 500;
color: var(--bg-white);
}
.rd-recent-label {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
}
.rd-recent-connect {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
border: none;
color: var(--bg-white);
cursor: pointer;
transition: background 0.2s ease, transform 0.2s ease;
}
.rd-recent-connect:hover {
background: var(--accent-cyan);
transform: scale(1.1);
}
.rd-recent-connect .material-icons {
font-size: 18px;
}
/* Quick Actions */ /* Quick Actions */
.rd-actions { .rd-actions {
display: flex; display: flex;
@ -266,13 +215,3 @@
.rd-action-btn .material-icons { .rd-action-btn .material-icons {
font-size: 16px; font-size: 16px;
} }
/* Scrollbar */
.rd-recent-section::-webkit-scrollbar {
width: 6px;
}
.rd-recent-section::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}

View File

@ -78,18 +78,20 @@
<button class="close-btn"><span class="material-icons">close</span></button> <button class="close-btn"><span class="material-icons">close</span></button>
</div> </div>
<!-- Connection Section --> <!-- Server Control -->
<div class="rd-connection-section"> <div class="rd-connection-section">
<h3>Connect to Remote Machine</h3> <h3>RustDesk Server</h3>
<div class="rd-input-row"> <div class="rd-server-control">
<input type="text" id="rd-peer-id" placeholder="Enter Peer ID or IP address..."> <div class="rd-server-info">
<button id="rd-connect-btn" class="rd-btn"> <span class="material-icons rd-server-icon">dns</span>
<span class="material-icons" style="font-size:18px">play_arrow</span> <div class="rd-server-details">
Connect <span class="rd-server-label">Local Server</span>
</button> <span class="rd-server-ports">Ports: 21115-21119</span>
<button id="rd-disconnect-btn" class="rd-btn"> </div>
<span class="material-icons" style="font-size:18px">stop</span> </div>
Disconnect <button id="rd-server-toggle" class="rd-btn rd-btn-server">
<span class="material-icons" style="font-size:18px">power_settings_new</span>
Start Server
</button> </button>
</div> </div>
</div> </div>
@ -97,14 +99,21 @@
<!-- Status Bar --> <!-- Status Bar -->
<div class="rd-status-bar"> <div class="rd-status-bar">
<div id="rd-status" class="rd-status-indicator rd-status-disconnected"></div> <div id="rd-status" class="rd-status-indicator rd-status-disconnected"></div>
<span id="rd-status-text">Not connected</span> <span id="rd-status-text">Server not running</span>
</div> </div>
<!-- Recent Connections --> <!-- Web Client Launcher -->
<div class="rd-recent-section"> <div class="rd-connection-section">
<h3>Recent Connections</h3> <h3>Remote Desktop Client</h3>
<div id="rd-connection-list"> <div class="rd-launch-section">
<!-- Recent connections dynamically added here --> <p class="rd-launch-desc">Open the RustDesk web client to connect to remote machines. The web client connects to the server via WebSocket.</p>
<div class="rd-input-row">
<input type="text" id="rd-server-url" placeholder="Server URL (e.g. localhost:21118)">
<button id="rd-launch-btn" class="rd-btn">
<span class="material-icons" style="font-size:18px">open_in_new</span>
Launch
</button>
</div>
</div> </div>
</div> </div>

View File

@ -3,16 +3,13 @@ export default class RemoteDesktop {
this.os = os; this.os = os;
this.element = document.getElementById('remote-desktop'); this.element = document.getElementById('remote-desktop');
this.closeBtn = this.element.querySelector('.close-btn'); this.closeBtn = this.element.querySelector('.close-btn');
this.connectBtn = this.element.querySelector('#rd-connect-btn'); this.launchBtn = this.element.querySelector('#rd-launch-btn');
this.disconnectBtn = this.element.querySelector('#rd-disconnect-btn'); this.serverToggle = this.element.querySelector('#rd-server-toggle');
this.peerIdInput = this.element.querySelector('#rd-peer-id');
this.statusIndicator = this.element.querySelector('#rd-status'); this.statusIndicator = this.element.querySelector('#rd-status');
this.statusText = this.element.querySelector('#rd-status-text'); this.statusText = this.element.querySelector('#rd-status-text');
this.connectionList = this.element.querySelector('#rd-connection-list'); this.serverUrlInput = this.element.querySelector('#rd-server-url');
this.isConnected = false; this.serverRunning = false;
this.currentPeerId = null;
this.recentConnections = [];
this.initialize(); this.initialize();
} }
@ -23,125 +20,96 @@ export default class RemoteDesktop {
this.os.closeAllPopups(); this.os.closeAllPopups();
}); });
this.connectBtn.addEventListener('click', (e) => { // Launch web client button
this.launchBtn.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
this.connectToPeer(); this.openWebClient();
}); });
this.disconnectBtn.addEventListener('click', (e) => { // Server toggle
this.serverToggle.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
this.disconnect(); this.toggleServer();
}); });
this.peerIdInput.addEventListener('keydown', (e) => { // Listen for server status events from Tauri
if (e.key === 'Enter') {
e.stopPropagation();
this.connectToPeer();
}
});
// Listen for status updates from Tauri backend
this.listenForEvents(); this.listenForEvents();
} }
async listenForEvents() { async listenForEvents() {
if (window.__TAURI__) { if (window.__TAURI__) {
const { listen } = await import('@tauri-apps/api/event'); try {
const { listen } = await import('@tauri-apps/api/event');
await listen('rustdesk-status', (event) => { await listen('rustdesk-status', (event) => {
const { running, pid } = event.payload; const { running, pid } = event.payload;
this.updateStatus(running, pid); this.updateServerStatus(running, pid);
}); });
} catch (e) {
await listen('rustdesk-connected', (event) => { console.warn('Tauri events not available:', e);
const { peer_id, pid } = event.payload; }
this.onConnected(peer_id, pid);
});
} }
} }
async connectToPeer() { async toggleServer() {
const peerId = this.peerIdInput.value.trim(); if (!window.__TAURI__) {
if (!peerId) { this.setStatus('error', 'Requires Tauri runtime');
this.setStatus('error', 'Please enter a peer ID');
return; return;
} }
this.setStatus('connecting', `Connecting to ${peerId}...`);
this.connectBtn.disabled = true;
try { try {
if (window.__TAURI__) { const { invoke } = await import('@tauri-apps/api/core');
const { invoke } = await import('@tauri-apps/api/core');
const result = await invoke('connect_to_peer', { peerId });
if (result.running) { if (this.serverRunning) {
this.currentPeerId = peerId; this.setStatus('connecting', 'Stopping server...');
this.addToRecent(peerId); const result = await invoke('stop_rustdesk_server');
this.onConnected(peerId, result.pid); this.updateServerStatus(result.running, result.pid);
} else {
this.setStatus('error', 'Failed to start RustDesk');
this.connectBtn.disabled = false;
}
} else { } else {
// Fallback for non-Tauri environments this.setStatus('connecting', 'Starting server...');
this.setStatus('error', 'RustDesk requires Tauri runtime'); const result = await invoke('start_rustdesk_server');
this.connectBtn.disabled = false; this.updateServerStatus(result.running, result.pid);
} }
} catch (err) { } catch (err) {
console.error('RustDesk connection error:', err); console.error('Server toggle error:', err);
this.setStatus('error', `Connection failed: ${err}`); this.setStatus('error', `Failed: ${err}`);
this.connectBtn.disabled = false; this.serverRunning = false;
this.serverToggle.classList.remove('rd-toggle-active');
this.serverToggle.textContent = 'Start Server';
} }
} }
async disconnect() { async openWebClient() {
this.setStatus('connecting', 'Launching web client...');
try { try {
if (window.__TAURI__) { if (window.__TAURI__) {
const { invoke } = await import('@tauri-apps/api/core'); const { invoke } = await import('@tauri-apps/api/core');
await invoke('stop_rustdesk'); const serverUrl = this.serverUrlInput?.value?.trim() || null;
await invoke('open_rustdesk_web', { serverUrl });
this.setStatus('connected', 'Web client opened');
} else {
// Dev fallback: open in new browser tab
const server = this.serverUrlInput?.value?.trim() || 'localhost';
window.open('https://web.rustdesk.com', '_blank');
this.setStatus('connected', 'Opened in browser');
} }
} catch (err) { } catch (err) {
console.error('RustDesk stop error:', err); console.error('Failed to open web client:', err);
this.setStatus('error', `Failed to launch: ${err}`);
} }
this.isConnected = false;
this.currentPeerId = null;
this.setStatus('disconnected', 'Disconnected');
this.connectBtn.disabled = false;
} }
async startRustdesk() { updateServerStatus(running, pid) {
try { this.serverRunning = running;
if (window.__TAURI__) { const toggle = this.serverToggle;
const { invoke } = await import('@tauri-apps/api/core');
const result = await invoke('start_rustdesk');
this.updateStatus(result.running, result.pid);
return result;
}
} catch (err) {
console.error('RustDesk start error:', err);
this.setStatus('error', `Failed to start: ${err}`);
}
return null;
}
onConnected(peerId, pid) {
this.isConnected = true;
this.currentPeerId = peerId;
this.setStatus('connected', `Connected to ${peerId} (PID: ${pid})`);
this.connectBtn.disabled = true;
}
updateStatus(running, pid) {
if (running) { if (running) {
this.setStatus('connected', `RustDesk running (PID: ${pid})`); this.setStatus('connected', `Server running (PID: ${pid})`);
this.isConnected = true; toggle.classList.add('rd-toggle-active');
toggle.textContent = 'Stop Server';
} else { } else {
this.setStatus('disconnected', 'RustDesk not running'); this.setStatus('disconnected', 'Server stopped');
this.isConnected = false; toggle.classList.remove('rd-toggle-active');
this.currentPeerId = null; toggle.textContent = 'Start Server';
this.connectBtn.disabled = false;
} }
} }
@ -149,84 +117,21 @@ export default class RemoteDesktop {
this.statusIndicator.className = 'rd-status-indicator'; this.statusIndicator.className = 'rd-status-indicator';
this.statusIndicator.classList.add(`rd-status-${state}`); this.statusIndicator.classList.add(`rd-status-${state}`);
this.statusText.textContent = text; this.statusText.textContent = text;
if (state === 'connected') {
this.disconnectBtn.style.display = 'flex';
this.connectBtn.style.display = 'none';
} else {
this.disconnectBtn.style.display = 'none';
this.connectBtn.style.display = 'flex';
}
} }
addToRecent(peerId) { async open() {
// Remove duplicate if exists
this.recentConnections = this.recentConnections.filter(id => id !== peerId);
// Add to front
this.recentConnections.unshift(peerId);
// Keep only last 5
if (this.recentConnections.length > 5) {
this.recentConnections = this.recentConnections.slice(0, 5);
}
this.renderRecentConnections();
}
renderRecentConnections() {
this.connectionList.innerHTML = '';
if (this.recentConnections.length === 0) {
this.connectionList.innerHTML = `
<div class="rd-empty-recent">
<span class="material-icons">history</span>
<span>No recent connections</span>
</div>
`;
return;
}
const fragment = document.createDocumentFragment();
this.recentConnections.forEach(peerId => {
const item = document.createElement('div');
item.className = 'rd-recent-item';
item.innerHTML = `
<div class="rd-recent-info">
<span class="material-icons rd-recent-icon">computer</span>
<div class="rd-recent-details">
<span class="rd-recent-id">${peerId}</span>
<span class="rd-recent-label">Remote Desktop</span>
</div>
</div>
<button class="rd-recent-connect">
<span class="material-icons">play_arrow</span>
</button>
`;
const connectBtn = item.querySelector('.rd-recent-connect');
connectBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.peerIdInput.value = peerId;
this.connectToPeer();
});
fragment.appendChild(item);
});
this.connectionList.appendChild(fragment);
}
open() {
this.element.classList.add('active'); this.element.classList.add('active');
this.os.components.taskbar.updateActiveButton('remoteDesktop'); this.os.components.taskbar.updateActiveButton('remoteDesktop');
this.renderRecentConnections();
// Check current status // Check current server status
if (window.__TAURI__) { if (window.__TAURI__) {
import('@tauri-apps/api/core').then(({ invoke }) => { try {
invoke('get_rustdesk_status').then(result => { const { invoke } = await import('@tauri-apps/api/core');
this.updateStatus(result.running, result.pid); const result = await invoke('get_rustdesk_status');
}).catch(() => {}); this.updateServerStatus(result.running, result.pid);
}); } catch (e) {
// Ignore - not running in Tauri
}
} }
} }