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 = [] }
serde = { version = "1", features = ["derive"] }
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`
- **macOS**: `rustdesk-universal-apple-darwin` → rename to `rustdesk`
```
Shelled OS (Tauri)
├── 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
cd shelled/rustdesk-as-ref
set VCPKG_ROOT=<your-vcpkg-install-path>
set VCPKG_ROOT=C:\path\to\vcpkg
cd shelled\rustdesk-as-ref
cargo build --release
```
The output binary will be at:
`target/release/rustdesk.exe`
Copy it to this directory:
### Copy binary
```powershell
mkdir shelled\shelled-os-ui\src-tauri\binaries
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 std::process::{Child, Command, Stdio};
use std::sync::Mutex;
use tauri::Manager;
use tauri::{Manager, WebviewUrl, WebviewWindowBuilder};
#[cfg(target_os = "windows")]
use std::os::windows::process::CommandExt;
/// State holding the RustDesk child process
/// State holding the RustDesk server process
struct RustDeskState {
process: Mutex<Option<Child>>,
}
@ -22,23 +22,20 @@ struct RustDeskStatus {
pid: Option<u32>,
}
/// Get the path to the RustDesk sidecar binary
/// Find the RustDesk binary
fn get_rustdesk_path() -> Result<std::path::PathBuf, String> {
let exe_dir = std::env::current_exe()
.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
.parent()
.ok_or("Failed to get parent directory of exe")?;
// Try multiple possible locations
let candidates = vec![
exe_dir.join("rustdesk.exe"), // Bundled sidecar (Windows)
exe_dir.join("binaries").join("rustdesk.exe"), // Explicit binaries dir
exe_dir.join("rustdesk"), // Bundled sidecar (Linux/macOS)
exe_dir.join("binaries").join("rustdesk"), // Explicit binaries dir (Linux/macOS)
exe_dir.join("rustdesk.exe"),
exe_dir.join("binaries").join("rustdesk.exe"),
exe_dir.join("rustdesk"),
exe_dir.join("binaries").join("rustdesk"),
];
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]
async fn start_rustdesk(
async fn start_rustdesk_server(
state: tauri::State<'_, RustDeskState>,
app: tauri::AppHandle,
) -> Result<RustDeskStatus, String> {
@ -64,10 +62,8 @@ async fn start_rustdesk(
// Already running?
if let Some(ref child) = *proc {
if let Ok(Some(_)) = child.try_wait() {
// Process exited, clean up
*proc = None;
} else {
// Still running
return Ok(RustDeskStatus {
running: true,
pid: Some(child.id()),
@ -77,29 +73,29 @@ async fn start_rustdesk(
let path = get_rustdesk_path()?;
// Start RustDesk in server mode (listens on 21115-21119)
#[cfg(target_os = "windows")]
const CREATE_NO_WINDOW: u32 = 0x08000000;
#[cfg(target_os = "windows")]
let child = Command::new(&path)
.arg("--connect")
.arg("--server")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.creation_flags(CREATE_NO_WINDOW)
.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"))]
let child = Command::new(&path)
.arg("--connect")
.arg("--server")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("Failed to start RustDesk: {}", e))?;
.map_err(|e| format!("Failed to start RustDesk server: {}", e))?;
let pid = child.id();
*proc = Some(child);
// Emit event to frontend
app.emit("rustdesk-status", RustDeskStatus {
running: true,
pid: Some(pid),
@ -112,9 +108,9 @@ async fn start_rustdesk(
})
}
/// Stop the RustDesk sidecar process
/// Stop the RustDesk server process
#[tauri::command]
async fn stop_rustdesk(
async fn stop_rustdesk_server(
state: tauri::State<'_, RustDeskState>,
app: tauri::AppHandle,
) -> Result<RustDeskStatus, String> {
@ -136,7 +132,7 @@ async fn stop_rustdesk(
Ok(status)
}
/// Get the current status of the RustDesk sidecar
/// Get the current server status
#[tauri::command]
async fn get_rustdesk_status(
state: tauri::State<'_, RustDeskState>,
@ -145,22 +141,18 @@ async fn get_rustdesk_status(
if let Some(ref mut child) = *proc {
match child.try_wait() {
Ok(Some(_status)) => {
// Process has exited
Ok(Some(_)) => {
*proc = None;
Ok(RustDeskStatus {
running: false,
pid: None,
})
}
Ok(None) => {
// Still running
Ok(RustDeskStatus {
Ok(None) => Ok(RustDeskStatus {
running: true,
pid: Some(child.id()),
})
}
Err(e) => Err(format!("Failed to check process status: {}", e)),
}),
Err(e) => Err(format!("Failed to check process: {}", e)),
}
} else {
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]
async fn connect_to_peer(
state: tauri::State<'_, RustDeskState>,
async fn open_rustdesk_web(
app: tauri::AppHandle,
peer_id: String,
) -> Result<RustDeskStatus, String> {
// Stop any existing instance
{
let mut proc = state.process.lock().map_err(|e| e.to_string())?;
if let Some(mut child) = proc.take() {
let _ = child.kill();
let _ = child.wait();
}
}
let path = get_rustdesk_path()?;
server_url: Option<String>,
) -> Result<(), String> {
// The RustDesk web client is served from the embedded rustdesk-web/ assets.
// In dev: looks at ../rustdesk-web/index.html
// In prod: bundled as Tauri resource
let web_path = std::env::current_dir()
.unwrap_or_default()
.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")]
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))?;
let url = format!("file:///{0}", path_str.replace('\\', "/"));
#[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 url = format!("file://{0}", path_str);
let pid = child.id();
{
let mut proc = state.process.lock().map_err(|e| e.to_string())?;
*proc = Some(child);
// Append server URL as query param if provided
if let Some(ref srv) = server_url {
format!("{}?server={}", url, srv)
} else {
url
}
let status = RustDeskStatus {
running: true,
pid: Some(pid),
} else {
// Fallback: try loading from Tauri asset protocol
// This requires the web client to be bundled via tauri.conf.json resources
"https://web.rustdesk.com".to_string()
};
app.emit("rustdesk-connected", serde_json::json!({
"peer_id": peer_id,
"pid": pid,
}))
.map_err(|e| e.to_string())?;
WebviewWindowBuilder::new(
&app,
"rustdesk-web",
tauri::WebviewUrl::External(url.parse().map_err(|e| format!("Invalid URL: {}", e))?),
)
.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() {
@ -235,10 +219,10 @@ fn main() {
process: Mutex::new(None),
})
.invoke_handler(tauri::generate_handler![
start_rustdesk,
stop_rustdesk,
start_rustdesk_server,
stop_rustdesk_server,
get_rustdesk_status,
connect_to_peer,
open_rustdesk_web,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

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

View File

@ -1,13 +1,85 @@
/* Remote Desktop Component Styles */
#remote-desktop {
left: 260px; /* Offset from remote desktop icon */
left: 200px;
width: 520px;
height: 560px;
height: 500px;
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 {
padding: 16px 20px;
border-bottom: 1px solid var(--glass-border);
@ -22,6 +94,18 @@
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 {
display: flex;
gap: 8px;
@ -50,45 +134,6 @@
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 */
.rd-status-bar {
display: flex;
@ -137,102 +182,6 @@
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 */
.rd-actions {
display: flex;
@ -266,13 +215,3 @@
.rd-action-btn .material-icons {
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>
</div>
<!-- Connection Section -->
<!-- Server Control -->
<div class="rd-connection-section">
<h3>Connect to Remote Machine</h3>
<div class="rd-input-row">
<input type="text" id="rd-peer-id" placeholder="Enter Peer ID or IP address...">
<button id="rd-connect-btn" class="rd-btn">
<span class="material-icons" style="font-size:18px">play_arrow</span>
Connect
</button>
<button id="rd-disconnect-btn" class="rd-btn">
<span class="material-icons" style="font-size:18px">stop</span>
Disconnect
<h3>RustDesk Server</h3>
<div class="rd-server-control">
<div class="rd-server-info">
<span class="material-icons rd-server-icon">dns</span>
<div class="rd-server-details">
<span class="rd-server-label">Local Server</span>
<span class="rd-server-ports">Ports: 21115-21119</span>
</div>
</div>
<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>
</div>
</div>
@ -97,14 +99,21 @@
<!-- Status Bar -->
<div class="rd-status-bar">
<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>
<!-- Recent Connections -->
<div class="rd-recent-section">
<h3>Recent Connections</h3>
<div id="rd-connection-list">
<!-- Recent connections dynamically added here -->
<!-- Web Client Launcher -->
<div class="rd-connection-section">
<h3>Remote Desktop Client</h3>
<div class="rd-launch-section">
<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>

View File

@ -3,16 +3,13 @@ export default class RemoteDesktop {
this.os = os;
this.element = document.getElementById('remote-desktop');
this.closeBtn = this.element.querySelector('.close-btn');
this.connectBtn = this.element.querySelector('#rd-connect-btn');
this.disconnectBtn = this.element.querySelector('#rd-disconnect-btn');
this.peerIdInput = this.element.querySelector('#rd-peer-id');
this.launchBtn = this.element.querySelector('#rd-launch-btn');
this.serverToggle = this.element.querySelector('#rd-server-toggle');
this.statusIndicator = this.element.querySelector('#rd-status');
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.currentPeerId = null;
this.recentConnections = [];
this.serverRunning = false;
this.initialize();
}
@ -23,125 +20,96 @@ export default class RemoteDesktop {
this.os.closeAllPopups();
});
this.connectBtn.addEventListener('click', (e) => {
// Launch web client button
this.launchBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.connectToPeer();
this.openWebClient();
});
this.disconnectBtn.addEventListener('click', (e) => {
// Server toggle
this.serverToggle.addEventListener('click', (e) => {
e.stopPropagation();
this.disconnect();
this.toggleServer();
});
this.peerIdInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.stopPropagation();
this.connectToPeer();
}
});
// Listen for status updates from Tauri backend
// Listen for server status events from Tauri
this.listenForEvents();
}
async listenForEvents() {
if (window.__TAURI__) {
try {
const { listen } = await import('@tauri-apps/api/event');
await listen('rustdesk-status', (event) => {
const { running, pid } = event.payload;
this.updateStatus(running, pid);
});
await listen('rustdesk-connected', (event) => {
const { peer_id, pid } = event.payload;
this.onConnected(peer_id, pid);
this.updateServerStatus(running, pid);
});
} catch (e) {
console.warn('Tauri events not available:', e);
}
}
}
async connectToPeer() {
const peerId = this.peerIdInput.value.trim();
if (!peerId) {
this.setStatus('error', 'Please enter a peer ID');
async toggleServer() {
if (!window.__TAURI__) {
this.setStatus('error', 'Requires Tauri runtime');
return;
}
this.setStatus('connecting', `Connecting to ${peerId}...`);
this.connectBtn.disabled = true;
try {
if (window.__TAURI__) {
const { invoke } = await import('@tauri-apps/api/core');
const result = await invoke('connect_to_peer', { peerId });
if (result.running) {
this.currentPeerId = peerId;
this.addToRecent(peerId);
this.onConnected(peerId, result.pid);
if (this.serverRunning) {
this.setStatus('connecting', 'Stopping server...');
const result = await invoke('stop_rustdesk_server');
this.updateServerStatus(result.running, result.pid);
} else {
this.setStatus('error', 'Failed to start RustDesk');
this.connectBtn.disabled = false;
this.setStatus('connecting', 'Starting server...');
const result = await invoke('start_rustdesk_server');
this.updateServerStatus(result.running, result.pid);
}
} catch (err) {
console.error('Server toggle error:', err);
this.setStatus('error', `Failed: ${err}`);
this.serverRunning = false;
this.serverToggle.classList.remove('rd-toggle-active');
this.serverToggle.textContent = 'Start Server';
}
}
async openWebClient() {
this.setStatus('connecting', 'Launching web client...');
try {
if (window.__TAURI__) {
const { invoke } = await import('@tauri-apps/api/core');
const serverUrl = this.serverUrlInput?.value?.trim() || null;
await invoke('open_rustdesk_web', { serverUrl });
this.setStatus('connected', 'Web client opened');
} else {
// Fallback for non-Tauri environments
this.setStatus('error', 'RustDesk requires Tauri runtime');
this.connectBtn.disabled = false;
// 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) {
console.error('RustDesk connection error:', err);
this.setStatus('error', `Connection failed: ${err}`);
this.connectBtn.disabled = false;
console.error('Failed to open web client:', err);
this.setStatus('error', `Failed to launch: ${err}`);
}
}
async disconnect() {
try {
if (window.__TAURI__) {
const { invoke } = await import('@tauri-apps/api/core');
await invoke('stop_rustdesk');
}
} catch (err) {
console.error('RustDesk stop error:', err);
}
updateServerStatus(running, pid) {
this.serverRunning = running;
const toggle = this.serverToggle;
this.isConnected = false;
this.currentPeerId = null;
this.setStatus('disconnected', 'Disconnected');
this.connectBtn.disabled = false;
}
async startRustdesk() {
try {
if (window.__TAURI__) {
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) {
this.setStatus('connected', `RustDesk running (PID: ${pid})`);
this.isConnected = true;
this.setStatus('connected', `Server running (PID: ${pid})`);
toggle.classList.add('rd-toggle-active');
toggle.textContent = 'Stop Server';
} else {
this.setStatus('disconnected', 'RustDesk not running');
this.isConnected = false;
this.currentPeerId = null;
this.connectBtn.disabled = false;
this.setStatus('disconnected', 'Server stopped');
toggle.classList.remove('rd-toggle-active');
toggle.textContent = 'Start Server';
}
}
@ -149,84 +117,21 @@ export default class RemoteDesktop {
this.statusIndicator.className = 'rd-status-indicator';
this.statusIndicator.classList.add(`rd-status-${state}`);
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) {
// 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() {
async open() {
this.element.classList.add('active');
this.os.components.taskbar.updateActiveButton('remoteDesktop');
this.renderRecentConnections();
// Check current status
// Check current server status
if (window.__TAURI__) {
import('@tauri-apps/api/core').then(({ invoke }) => {
invoke('get_rustdesk_status').then(result => {
this.updateStatus(result.running, result.pid);
}).catch(() => {});
});
try {
const { invoke } = await import('@tauri-apps/api/core');
const result = await invoke('get_rustdesk_status');
this.updateServerStatus(result.running, result.pid);
} catch (e) {
// Ignore - not running in Tauri
}
}
}