Files
Triple-C/app/src-tauri/src/commands/web_terminal_commands.rs
Josh Knapp 922543cc04
All checks were successful
Build App / compute-version (push) Successful in 3s
Build App / build-macos (push) Successful in 2m36s
Build App / build-windows (push) Successful in 4m39s
Build App / build-linux (push) Successful in 5m56s
Build App / create-tag (push) Successful in 2s
Build App / sync-to-github (push) Successful in 10s
Add web terminal for remote tablet/phone access to project terminals
Adds an axum HTTP+WebSocket server that runs alongside the Tauri app,
serving a standalone xterm.js-based terminal UI accessible from any
browser on the local network. Shares the existing ExecSessionManager
via Arc-wrapped stores, with token-based authentication and automatic
session cleanup on disconnect.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 19:31:16 -07:00

144 lines
4.4 KiB
Rust

use serde::Serialize;
use tauri::State;
use crate::web_terminal::WebTerminalServer;
use crate::AppState;
#[derive(Serialize)]
pub struct WebTerminalInfo {
pub running: bool,
pub port: u16,
pub access_token: String,
pub local_ip: Option<String>,
pub url: Option<String>,
}
fn generate_token() -> String {
use rand::Rng;
let mut rng = rand::rng();
let bytes: Vec<u8> = (0..32).map(|_| rng.random::<u8>()).collect();
use base64::Engine;
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&bytes)
}
fn get_local_ip() -> Option<String> {
local_ip_address::local_ip().ok().map(|ip| ip.to_string())
}
fn build_info(running: bool, port: u16, token: &str) -> WebTerminalInfo {
let local_ip = get_local_ip();
let url = if running {
local_ip
.as_ref()
.map(|ip| format!("http://{}:{}?token={}", ip, port, token))
} else {
None
};
WebTerminalInfo {
running,
port,
access_token: token.to_string(),
local_ip,
url,
}
}
#[tauri::command]
pub async fn start_web_terminal(state: State<'_, AppState>) -> Result<WebTerminalInfo, String> {
let mut server_guard = state.web_terminal_server.lock().await;
if server_guard.is_some() {
return Err("Web terminal server is already running".to_string());
}
let mut settings = state.settings_store.get();
// Auto-generate token if not set
if settings.web_terminal.access_token.is_none() {
settings.web_terminal.access_token = Some(generate_token());
settings.web_terminal.enabled = true;
state.settings_store.update(settings.clone()).map_err(|e| format!("Failed to save settings: {}", e))?;
}
let token = settings.web_terminal.access_token.clone().unwrap_or_default();
let port = settings.web_terminal.port;
let server = WebTerminalServer::start(
port,
token.clone(),
state.exec_manager.clone(),
state.projects_store.clone(),
state.settings_store.clone(),
)
.await?;
*server_guard = Some(server);
// Mark as enabled in settings
if !settings.web_terminal.enabled {
settings.web_terminal.enabled = true;
let _ = state.settings_store.update(settings);
}
Ok(build_info(true, port, &token))
}
#[tauri::command]
pub async fn stop_web_terminal(state: State<'_, AppState>) -> Result<(), String> {
let mut server_guard = state.web_terminal_server.lock().await;
if let Some(server) = server_guard.take() {
server.stop();
}
// Mark as disabled in settings
let mut settings = state.settings_store.get();
if settings.web_terminal.enabled {
settings.web_terminal.enabled = false;
let _ = state.settings_store.update(settings);
}
Ok(())
}
#[tauri::command]
pub async fn get_web_terminal_status(state: State<'_, AppState>) -> Result<WebTerminalInfo, String> {
let server_guard = state.web_terminal_server.lock().await;
let settings = state.settings_store.get();
let token = settings.web_terminal.access_token.clone().unwrap_or_default();
let running = server_guard.is_some();
Ok(build_info(running, settings.web_terminal.port, &token))
}
#[tauri::command]
pub async fn regenerate_web_terminal_token(state: State<'_, AppState>) -> Result<WebTerminalInfo, String> {
// Stop current server if running
{
let mut server_guard = state.web_terminal_server.lock().await;
if let Some(server) = server_guard.take() {
server.stop();
}
}
// Generate new token and save
let new_token = generate_token();
let mut settings = state.settings_store.get();
settings.web_terminal.access_token = Some(new_token.clone());
state.settings_store.update(settings.clone()).map_err(|e| format!("Failed to save settings: {}", e))?;
// Restart if was enabled
if settings.web_terminal.enabled {
let server = WebTerminalServer::start(
settings.web_terminal.port,
new_token.clone(),
state.exec_manager.clone(),
state.projects_store.clone(),
state.settings_store.clone(),
)
.await?;
let mut server_guard = state.web_terminal_server.lock().await;
*server_guard = Some(server);
return Ok(build_info(true, settings.web_terminal.port, &new_token));
}
Ok(build_info(false, settings.web_terminal.port, &new_token))
}