diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a8698c6 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "shelled/rustdesk-as-ref/libs/hbb_common"] + path = shelled/rustdesk-as-ref/libs/hbb_common + url = https://github.com/rustdesk/hbb_common diff --git a/shelled/rustdesk-as-ref/libs/hbb_common b/shelled/rustdesk-as-ref/libs/hbb_common new file mode 160000 index 0000000..f08ce5d --- /dev/null +++ b/shelled/rustdesk-as-ref/libs/hbb_common @@ -0,0 +1 @@ +Subproject commit f08ce5d6d07cd200713418ce2932769d14ff21d2 diff --git a/shelled/shelled-os-ui/src-tauri/Cargo.toml b/shelled/shelled-os-ui/src-tauri/Cargo.toml index 564b1db..dd35a53 100644 --- a/shelled/shelled-os-ui/src-tauri/Cargo.toml +++ b/shelled/shelled-os-ui/src-tauri/Cargo.toml @@ -12,3 +12,4 @@ tauri-build = { version = "2", features = [] } tauri = { version = "2", features = [] } serde = { version = "1", features = ["derive"] } serde_json = "1" +tokio = { version = "1", features = ["full"] } diff --git a/shelled/shelled-os-ui/src-tauri/binaries/README.md b/shelled/shelled-os-ui/src-tauri/binaries/README.md new file mode 100644 index 0000000..190d2ba --- /dev/null +++ b/shelled/shelled-os-ui/src-tauri/binaries/README.md @@ -0,0 +1,29 @@ +# RustDesk Sidecar Binary + +Place the compiled RustDesk binary here: + +- **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` + +## How to build RustDesk + +See `shelled/rustdesk-as-ref/CLAUDE.md` for build instructions. + +### Quick build (Windows with vcpkg) + +```powershell +cd shelled/rustdesk-as-ref +set VCPKG_ROOT= +cargo build --release +``` + +The output binary will be at: +`target/release/rustdesk.exe` + +Copy it to this directory: +```powershell +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`. diff --git a/shelled/shelled-os-ui/src-tauri/capabilities/default.json b/shelled/shelled-os-ui/src-tauri/capabilities/default.json index b9ec4c1..93f2788 100644 --- a/shelled/shelled-os-ui/src-tauri/capabilities/default.json +++ b/shelled/shelled-os-ui/src-tauri/capabilities/default.json @@ -4,6 +4,10 @@ "description": "Capability for the main window", "windows": ["main"], "permissions": [ - "core:default" + "core:default", + "shell:allow-spawn", + "shell:allow-open", + "core:event:default", + "core:window:default" ] } diff --git a/shelled/shelled-os-ui/src-tauri/src/main.rs b/shelled/shelled-os-ui/src-tauri/src/main.rs index 860b10d..ff4597f 100644 --- a/shelled/shelled-os-ui/src-tauri/src/main.rs +++ b/shelled/shelled-os-ui/src-tauri/src/main.rs @@ -3,8 +3,243 @@ windows_subsystem = "windows" )] +use serde::{Deserialize, Serialize}; +use std::process::{Child, Command, Stdio}; +use std::sync::Mutex; +use tauri::Manager; + +#[cfg(target_os = "windows")] +use std::os::windows::process::CommandExt; + +/// State holding the RustDesk child process +struct RustDeskState { + process: Mutex>, +} + +#[derive(Serialize, Deserialize, Clone)] +struct RustDeskStatus { + running: bool, + pid: Option, +} + +/// Get the path to the RustDesk sidecar binary +fn get_rustdesk_path() -> Result { + 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) + ]; + + for candidate in &candidates { + if candidate.exists() { + return Ok(candidate.clone()); + } + } + + Err(format!( + "RustDesk binary not found. Tried: {:?}", + candidates + )) +} + +/// Start the RustDesk sidecar process +#[tauri::command] +async fn start_rustdesk( + state: tauri::State<'_, RustDeskState>, + app: tauri::AppHandle, +) -> Result { + let mut proc = state.process.lock().map_err(|e| e.to_string())?; + + // 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()), + }); + } + } + + let path = get_rustdesk_path()?; + + #[cfg(target_os = "windows")] + const CREATE_NO_WINDOW: u32 = 0x08000000; + #[cfg(target_os = "windows")] + let child = Command::new(&path) + .arg("--connect") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .creation_flags(CREATE_NO_WINDOW) + .spawn() + .map_err(|e| format!("Failed to start RustDesk: {}", e))?; + + #[cfg(not(target_os = "windows"))] + let child = Command::new(&path) + .arg("--connect") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| format!("Failed to start RustDesk: {}", e))?; + + let pid = child.id(); + *proc = Some(child); + + // Emit event to frontend + app.emit("rustdesk-status", RustDeskStatus { + running: true, + pid: Some(pid), + }) + .map_err(|e| e.to_string())?; + + Ok(RustDeskStatus { + running: true, + pid: Some(pid), + }) +} + +/// Stop the RustDesk sidecar process +#[tauri::command] +async fn stop_rustdesk( + state: tauri::State<'_, RustDeskState>, + app: tauri::AppHandle, +) -> Result { + let mut proc = state.process.lock().map_err(|e| e.to_string())?; + + if let Some(mut child) = proc.take() { + child.kill().map_err(|e| format!("Failed to kill RustDesk: {}", e))?; + let _ = child.wait(); + } + + let status = RustDeskStatus { + running: false, + pid: None, + }; + + app.emit("rustdesk-status", status.clone()) + .map_err(|e| e.to_string())?; + + Ok(status) +} + +/// Get the current status of the RustDesk sidecar +#[tauri::command] +async fn get_rustdesk_status( + state: tauri::State<'_, RustDeskState>, +) -> Result { + let mut proc = state.process.lock().map_err(|e| e.to_string())?; + + if let Some(ref mut child) = *proc { + match child.try_wait() { + Ok(Some(_status)) => { + // Process has exited + *proc = None; + Ok(RustDeskStatus { + running: false, + pid: None, + }) + } + Ok(None) => { + // Still running + Ok(RustDeskStatus { + running: true, + pid: Some(child.id()), + }) + } + Err(e) => Err(format!("Failed to check process status: {}", e)), + } + } else { + Ok(RustDeskStatus { + running: false, + pid: None, + }) + } +} + +/// Connect to a remote peer via RustDesk +#[tauri::command] +async fn connect_to_peer( + state: tauri::State<'_, RustDeskState>, + app: tauri::AppHandle, + peer_id: String, +) -> Result { + // 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()?; + + #[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!({ + "peer_id": peer_id, + "pid": pid, + })) + .map_err(|e| e.to_string())?; + + Ok(status) +} + fn main() { tauri::Builder::default() + .manage(RustDeskState { + process: Mutex::new(None), + }) + .invoke_handler(tauri::generate_handler![ + start_rustdesk, + stop_rustdesk, + get_rustdesk_status, + connect_to_peer, + ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/shelled/shelled-os-ui/src-tauri/tauri.conf.json b/shelled/shelled-os-ui/src-tauri/tauri.conf.json index d365dd3..a510126 100644 --- a/shelled/shelled-os-ui/src-tauri/tauri.conf.json +++ b/shelled/shelled-os-ui/src-tauri/tauri.conf.json @@ -18,5 +18,13 @@ "security": { "csp": null } + }, + "bundle": { + "externalBin": [ + "binaries/rustdesk" + ], + "resources": [ + "binaries/*" + ] } } diff --git a/shelled/shelled-os-ui/src/css/remote-desktop.css b/shelled/shelled-os-ui/src/css/remote-desktop.css new file mode 100644 index 0000000..d6541ca --- /dev/null +++ b/shelled/shelled-os-ui/src/css/remote-desktop.css @@ -0,0 +1,278 @@ +/* Remote Desktop Component Styles */ + +#remote-desktop { + left: 260px; /* Offset from remote desktop icon */ + width: 520px; + height: 560px; + z-index: 95; +} + +/* Connection Section */ +.rd-connection-section { + padding: 16px 20px; + border-bottom: 1px solid var(--glass-border); +} + +.rd-connection-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-input-row { + display: flex; + gap: 8px; + align-items: center; +} + +.rd-input-row input { + flex-grow: 1; + background: rgba(0, 0, 0, 0.2); + border: 1px solid var(--glass-border); + border-radius: 8px; + padding: 10px 14px; + color: var(--bg-white); + font-family: 'Outfit', sans-serif; + font-size: 0.95rem; + outline: none; + transition: border-color 0.2s ease, background 0.2s ease; +} + +.rd-input-row input:focus { + border-color: var(--accent-cyan); + background: rgba(0, 0, 0, 0.3); +} + +.rd-input-row input::placeholder { + 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; + align-items: center; + gap: 10px; + padding: 12px 20px; + background: rgba(0, 0, 0, 0.1); + border-bottom: 1px solid var(--glass-border); +} + +.rd-status-indicator { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + transition: background 0.3s ease, box-shadow 0.3s ease; +} + +.rd-status-disconnected { + background: rgba(255, 255, 255, 0.3); +} + +.rd-status-connecting { + background: #ffd200; + box-shadow: 0 0 8px rgba(255, 210, 0, 0.6); + animation: rd-pulse 1.2s ease-in-out infinite; +} + +.rd-status-connected { + background: #38ef7d; + box-shadow: 0 0 8px rgba(56, 239, 125, 0.6); +} + +.rd-status-error { + background: #ff4444; + box-shadow: 0 0 8px rgba(255, 68, 68, 0.6); +} + +@keyframes rd-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} + +#rd-status-text { + font-size: 0.85rem; + 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; + gap: 8px; + padding: 12px 20px; + border-top: 1px solid var(--glass-border); +} + +.rd-action-btn { + display: flex; + align-items: center; + gap: 6px; + flex: 1; + justify-content: center; + padding: 8px 12px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + color: rgba(255, 255, 255, 0.8); + font-family: 'Outfit', sans-serif; + font-size: 0.8rem; + cursor: pointer; + transition: background 0.2s ease, color 0.2s ease; +} + +.rd-action-btn:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--bg-white); +} + +.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; +} diff --git a/shelled/shelled-os-ui/src/index.html b/shelled/shelled-os-ui/src/index.html index 60db01c..7531db0 100644 --- a/shelled/shelled-os-ui/src/index.html +++ b/shelled/shelled-os-ui/src/index.html @@ -13,6 +13,7 @@ +
@@ -69,6 +70,60 @@
+ + + @@ -83,6 +138,9 @@ + diff --git a/shelled/shelled-os-ui/src/js/components/RemoteDesktop.js b/shelled/shelled-os-ui/src/js/components/RemoteDesktop.js new file mode 100644 index 0000000..67bc40c --- /dev/null +++ b/shelled/shelled-os-ui/src/js/components/RemoteDesktop.js @@ -0,0 +1,237 @@ +export default class RemoteDesktop { + constructor(os) { + 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.statusIndicator = this.element.querySelector('#rd-status'); + this.statusText = this.element.querySelector('#rd-status-text'); + this.connectionList = this.element.querySelector('#rd-connection-list'); + + this.isConnected = false; + this.currentPeerId = null; + this.recentConnections = []; + + this.initialize(); + } + + initialize() { + this.closeBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.os.closeAllPopups(); + }); + + this.connectBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.connectToPeer(); + }); + + this.disconnectBtn.addEventListener('click', (e) => { + e.stopPropagation(); + this.disconnect(); + }); + + this.peerIdInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.stopPropagation(); + this.connectToPeer(); + } + }); + + // Listen for status updates from Tauri backend + this.listenForEvents(); + } + + async listenForEvents() { + if (window.__TAURI__) { + 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); + }); + } + } + + async connectToPeer() { + const peerId = this.peerIdInput.value.trim(); + if (!peerId) { + this.setStatus('error', 'Please enter a peer ID'); + 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); + } else { + this.setStatus('error', 'Failed to start RustDesk'); + this.connectBtn.disabled = false; + } + } else { + // Fallback for non-Tauri environments + this.setStatus('error', 'RustDesk requires Tauri runtime'); + this.connectBtn.disabled = false; + } + } catch (err) { + console.error('RustDesk connection error:', err); + this.setStatus('error', `Connection failed: ${err}`); + this.connectBtn.disabled = false; + } + } + + 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); + } + + 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; + } else { + this.setStatus('disconnected', 'RustDesk not running'); + this.isConnected = false; + this.currentPeerId = null; + this.connectBtn.disabled = false; + } + } + + setStatus(state, text) { + 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 = ` +
+ history + No recent connections +
+ `; + return; + } + + const fragment = document.createDocumentFragment(); + + this.recentConnections.forEach(peerId => { + const item = document.createElement('div'); + item.className = 'rd-recent-item'; + item.innerHTML = ` +
+ computer +
+ ${peerId} + Remote Desktop +
+
+ + `; + + 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.os.components.taskbar.updateActiveButton('remoteDesktop'); + this.renderRecentConnections(); + + // Check current status + if (window.__TAURI__) { + import('@tauri-apps/api/core').then(({ invoke }) => { + invoke('get_rustdesk_status').then(result => { + this.updateStatus(result.running, result.pid); + }).catch(() => {}); + }); + } + } + + close() { + this.element.classList.remove('active'); + this.os.components.taskbar.updateActiveButton(null); + } +} diff --git a/shelled/shelled-os-ui/src/js/components/Taskbar.js b/shelled/shelled-os-ui/src/js/components/Taskbar.js index 3c8c18b..2180cdd 100644 --- a/shelled/shelled-os-ui/src/js/components/Taskbar.js +++ b/shelled/shelled-os-ui/src/js/components/Taskbar.js @@ -5,6 +5,7 @@ export default class Taskbar { start: document.getElementById('btn-start'), explorer: document.getElementById('btn-explorer'), browser: document.getElementById('btn-browser'), + rustdesk: document.getElementById('btn-rustdesk'), settings: document.getElementById('btn-settings') }; this.clockElement = document.getElementById('clock'); @@ -28,6 +29,11 @@ export default class Taskbar { this.os.togglePopup('browser'); }); + // Remote Desktop Button + this.buttons.rustdesk.addEventListener('click', (e) => { + this.os.togglePopup('remoteDesktop'); + }); + // Settings Button this.buttons.settings.addEventListener('click', (e) => { this.os.togglePopup('settings'); @@ -45,6 +51,7 @@ export default class Taskbar { 'startMenu': this.buttons.start, 'fileExplorer': this.buttons.explorer, 'browser': this.buttons.browser, + 'remoteDesktop': this.buttons.rustdesk, 'settings': this.buttons.settings }; diff --git a/shelled/shelled-os-ui/src/js/main.js b/shelled/shelled-os-ui/src/js/main.js index 6fc3344..d05b8f1 100644 --- a/shelled/shelled-os-ui/src/js/main.js +++ b/shelled/shelled-os-ui/src/js/main.js @@ -3,6 +3,7 @@ import StartMenu from './components/StartMenu.js'; import FileExplorer from './components/FileExplorer.js'; import Browser from './components/Browser.js'; import Settings from './components/Settings.js'; +import RemoteDesktop from './components/RemoteDesktop.js'; class OSController { constructor() { @@ -16,6 +17,7 @@ class OSController { this.components.startMenu = new StartMenu(this); this.components.fileExplorer = new FileExplorer(this); this.components.browser = new Browser(this); + this.components.remoteDesktop = new RemoteDesktop(this); this.components.settings = new Settings(this); // Taskbar comes last as it needs references to others