tauri-remote-ui 1.0.1-alpha.4

A Tauri plugin that exposes the application’s UI to a web browser, allowing full interaction while the native app continues running. This enables frontend debug, end-to-end UI testing using existing web-based testing tools without requiring modifications to the app itself.
// AGPL-3.0-only License
// Copyright (c) 2025 DraviaVemal
// See LICENSE file in the root directory.

use crate::{RpcServer, WsPayload};
use futures::{stream::SplitSink, SinkExt};
use hyper::upgrade::Upgraded;
use hyper_tungstenite::{tungstenite::Message, WebSocketStream};
use hyper_util::rt::TokioIo;
use serde::{de::DeserializeOwned, Serialize};
use serde_json::json;
use std::sync::Arc;
use tauri::{plugin::PluginApi, AppHandle, Error, Listener, Manager, Runtime};
use tokio::sync::{Mutex, RwLock};

pub fn init<R, C>(app: &AppHandle, _api: PluginApi<R, C>) -> crate::Result<Arc<RwLock<RemoteUi>>>
where
    C: DeserializeOwned,
    R: Runtime,
{
    let app_handle = Arc::new(app.clone());
    let remote_ui = Arc::new(RwLock::new(RemoteUi {
        app: app_handle.clone(),
        rpc_server: RpcServer::new(app_handle),
    }));
    Ok(remote_ui)
}

#[derive(Debug)]
/// Access to the remote-ui APIs.
pub struct RemoteUi {
    pub(crate) app: Arc<AppHandle>,
    pub(crate) rpc_server: RpcServer,
}

impl RemoteUi {
    pub(crate) fn is_rpc_active(&self) -> bool {
        self.rpc_server.get_is_active()
    }

    pub(crate) fn invoke_rpc(
        &self,
        payload: String,
        session: Arc<Mutex<SplitSink<WebSocketStream<TokioIo<Upgraded>>, Message>>>,
    ) -> Result<(), Error> {
        let ws_payload: WsPayload = serde_json::from_str(&payload)?;
        let window = self
            .app
            .get_webview_window("main")
            .ok_or(Error::AssetNotFound("WebviewWindow Not Found".to_owned()))?;
        let req_unique_id = format!("remote-ui::result::{}", &ws_payload.id);
        self.app
            .app_handle()
            .once_any(&req_unique_id, move |handler| {
                // Spawn a new task to send the message asynchronously
                let payload = handler.payload().to_string();
                let id = ws_payload.id;
                tauri::async_runtime::spawn(async move {
                    if let Err(err) = session
                        .lock()
                        .await
                        .send(Message::text(
                            json!({"id":id,"payload":payload}).to_string(),
                        ))
                        .await
                    {
                        eprintln!("WS Send Message Failed. Err:{err}");
                    }
                });
            });
        let js = format!(
            r#"
            window.__TAURI_INTERNALS__.invoke("{}",{},{})
                .then((res) => {{
                        window.__TAURI_INTERNALS__.invoke("plugin:event|emit",{{
                        event:"{}",
                        payload:{{
                            status: "success",
                            payload:res
                        }}
                    }})
                }})
                .catch((err) => {{
                    window.__TAURI_INTERNALS__.invoke("plugin:event|emit",{{
                        event:"{}",
                        payload:{{
                            status: "error",
                            payload:err
                        }}
                    }})
                }});
            "#,
            ws_payload.cmd,
            serde_json::to_string(&ws_payload.args).map_err(|err| {
                Error::PluginInitialization(
                    "tauri-remote-ui".to_owned(),
                    format!("Failed to parse message. Err: {err}"),
                )
            })?,
            serde_json::to_string(&ws_payload.option).map_err(|err| {
                Error::PluginInitialization(
                    "tauri-remote-ui".to_owned(),
                    format!("Failed to parse message. Err: {err}"),
                )
            })?,
            &req_unique_id,
            &req_unique_id
        );
        window.eval(js)?;
        Ok(())
    }

    /// Emit message to target window to listen
    pub async fn emit<P: Serialize + Clone>(&self, event: &str, payload: P) -> Result<(), Error> {
        if let Some(session) = self.rpc_server.get_ws_handle("main") {
            let json = json!({
                "event":event,
                "payload":payload
            })
            .to_string();
            session
                .lock()
                .await
                .send(Message::text(json))
                .await
                .map_err(|err| {
                    Error::PluginInitialization(
                        "tauri-remote-ui".to_owned(),
                        format!("Failed to send WS message. Err: {err}"),
                    )
                })?;
        }
        Ok(())
    }
}