Comandos de Rust | vasak-desktop
Guía para desarrollar comandos IPC (Tauri) en Rust.
Concepto: Comandos Tauri
Los comandos son funciones Rust que se pueden llamar desde el frontend Vue.js a través de IPC.
sequenceDiagram
participant Frontend as 🎨 Frontend
(Vue.js)
participant Invoke as invoke()
participant Bridge as 🔗 Tauri Bridge
participant IPC as 📡 IPC Channel
participant Backend as ⚙️ Backend
(Rust)
Frontend->>Invoke: invoke('command_name', data)
activate Invoke
Invoke->>Bridge: serializa datos
deactivate Invoke
activate Bridge
Bridge->>IPC: envía por canal
deactivate Bridge
activate IPC
IPC->>Backend: entrega comando
deactivate IPC
activate Backend
Backend->>Backend: ejecuta función Rust
Backend->>IPC: retorna resultado
deactivate Backend
activate IPC
IPC->>Bridge: envía respuesta
deactivate IPC
activate Bridge
Bridge->>Invoke: deserializa resultado
deactivate Bridge
activate Invoke
Invoke->>Frontend: Promise resuelto
deactivate Invoke
Note over Frontend,Backend: Comunicación bidireccional segura
Estructura Base de un Comando
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
// src-tauri/src/commands/mod.rs
use tauri::State;
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
pub struct VolumeLevel {
pub level: u32,
}
// Comando simple
#[tauri::command]
pub fn get_version() -> String {
"0.5.2".to_string()
}
// Comando con parámetros
#[tauri::command]
pub fn set_volume(level: u32) -> Result<(), String> {
if level > 100 {
return Err("Volume must be 0-100".to_string());
}
// Lógica para cambiar volumen
println!("Volumen establecido a: {}", level);
Ok(())
}
// Comando async
#[tauri::command]
pub async fn load_devices() -> Result<Vec<Device>, String> {
// Operación async
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
Ok(vec![])
}
// Comando con estado compartido
#[tauri::command]
pub fn get_config(state: State<AppConfig>) -> Result<String, String> {
Ok(state.config_path.clone())
}
|
Registrar Comandos
En src/main.rs o src/lib.rs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
use tauri::Manager;
mod commands;
use commands::*;
fn main() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![
// Registra aquí
get_version,
set_volume,
load_devices,
get_config,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
|
Tipos de Datos
Tipos Simples
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#[tauri::command]
pub fn handle_int(value: i32) -> i32 {
value * 2
}
#[tauri::command]
pub fn handle_string(text: String) -> String {
format!("Echo: {}", text)
}
#[tauri::command]
pub fn handle_bool(flag: bool) -> bool {
!flag
}
#[tauri::command]
pub fn handle_float(value: f64) -> f64 {
value.sqrt()
}
|
Tipos Complejos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
pub struct Device {
pub id: String,
pub name: String,
pub volume: u32,
}
#[derive(Serialize, Deserialize)]
pub struct AudioSettings {
pub volume: u32,
pub muted: bool,
pub device: String,
}
#[tauri::command]
pub fn get_audio_settings() -> AudioSettings {
AudioSettings {
volume: 50,
muted: false,
device: "default".to_string(),
}
}
#[tauri::command]
pub fn update_audio_settings(settings: AudioSettings) -> Result<(), String> {
// Actualizar configuración
Ok(())
}
#[tauri::command]
pub fn get_devices() -> Vec<Device> {
vec![
Device {
id: "dev1".to_string(),
name: "Speaker".to_string(),
volume: 50,
},
]
}
|
Manejo de Errores
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
// ✅ Retornar error explícitamente
#[tauri::command]
pub fn validate_input(input: String) -> Result<String, String> {
if input.is_empty() {
return Err("Input cannot be empty".to_string());
}
Ok(input.to_uppercase())
}
// ✅ Usar custom error type
#[derive(Debug, Serialize)]
pub enum CommandError {
#[serde(rename = "device_not_found")]
DeviceNotFound,
#[serde(rename = "invalid_volume")]
InvalidVolume,
#[serde(rename = "dbus_error")]
DbusError(String),
}
impl std::fmt::Display for CommandError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
CommandError::DeviceNotFound => write!(f, "Device not found"),
CommandError::InvalidVolume => write!(f, "Volume out of range"),
CommandError::DbusError(msg) => write!(f, "D-Bus error: {}", msg),
}
}
}
#[tauri::command]
pub fn set_audio_device(device_id: String) -> Result<(), CommandError> {
// Validar device
if device_id.is_empty() {
return Err(CommandError::DeviceNotFound);
}
Ok(())
}
|
Comandos Async
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
use tokio::time::{sleep, Duration};
// Comando async simple
#[tauri::command]
pub async fn fetch_network_status() -> Result<NetworkStatus, String> {
sleep(Duration::from_secs(2)).await;
Ok(NetworkStatus {
connected: true,
signal: 85,
})
}
// Async con operaciones I/O
#[tauri::command]
pub async fn read_config_file() -> Result<String, String> {
let content = std::fs::read_to_string("config.toml")
.map_err(|e| format!("Failed to read config: {}", e))?;
Ok(content)
}
// Async con timeout
#[tauri::command]
pub async fn get_device_list() -> Result<Vec<Device>, String> {
match tokio::time::timeout(
Duration::from_secs(5),
fetch_devices_from_dbus()
).await {
Ok(Ok(devices)) => Ok(devices),
Ok(Err(e)) => Err(e),
Err(_) => Err("Operation timed out".to_string()),
}
}
async fn fetch_devices_from_dbus() -> Result<Vec<Device>, String> {
// Operación async
Ok(vec![])
}
|
Acceso al Estado Compartido
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
use tauri::State;
use std::sync::Mutex;
pub struct AppConfig {
pub config_path: String,
}
pub struct AudioState {
pub current_volume: Mutex<u32>,
}
#[tauri::command]
pub fn get_config_path(state: State<AppConfig>) -> String {
state.config_path.clone()
}
#[tauri::command]
pub fn get_volume(state: State<AudioState>) -> u32 {
*state.current_volume.lock().unwrap()
}
#[tauri::command]
pub fn set_volume(level: u32, state: State<AudioState>) -> Result<(), String> {
*state.current_volume.lock().unwrap() = level;
Ok(())
}
// En main.rs
#[tauri::command]
fn main() {
let audio_state = AudioState {
current_volume: Mutex::new(50),
};
tauri::Builder::default()
.manage(AppConfig {
config_path: "/home/user/.config".to_string(),
})
.manage(audio_state)
.invoke_handler(tauri::generate_handler![
get_config_path,
get_volume,
set_volume,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
|
Emitir Eventos al Frontend
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
use tauri::Manager;
// Emitir evento a una ventana específica
#[tauri::command]
pub fn notify_volume_change(
window: tauri::Window,
new_volume: u32,
) -> Result<(), String> {
window.emit("volume_changed", new_volume)
.map_err(|e| e.to_string())?;
Ok(())
}
// Emitir evento a todas las ventanas
#[tauri::command]
pub fn notify_globally(
app: tauri::AppHandle,
message: String,
) -> Result<(), String> {
app.emit_all("global_event", message)
.map_err(|e| e.to_string())?;
Ok(())
}
// Emitir de forma asíncrona
#[tauri::command]
pub async fn long_operation(window: tauri::Window) -> Result<String, String> {
for i in 0..10 {
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
window.emit("progress", i)
.map_err(|e| e.to_string())?;
}
Ok("Completado".to_string())
}
|
Acceso al Filesystem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
use tauri::api::path;
use std::fs;
#[tauri::command]
pub fn save_config(content: String) -> Result<(), String> {
let config_dir = path::config_dir()
.ok_or("Cannot find config dir")?;
let config_path = config_dir.join("vasak").join("config.toml");
fs::create_dir_all(config_path.parent().unwrap())
.map_err(|e| e.to_string())?;
fs::write(&config_path, content)
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub fn load_config() -> Result<String, String> {
let config_dir = path::config_dir()
.ok_or("Cannot find config dir")?;
let config_path = config_dir.join("vasak").join("config.toml");
fs::read_to_string(&config_path)
.map_err(|e| e.to_string())
}
|
Integración con D-Bus
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
use zbus::Connection;
#[tauri::command]
pub async fn get_volume_from_dbus() -> Result<u32, String> {
// Conectar al bus de sesión
let connection = Connection::session()
.await
.map_err(|e| format!("D-Bus connection failed: {}", e))?;
// Obtener información
let volume = dbus_service::get_volume(&connection)
.await
.map_err(|e| e.to_string())?;
Ok(volume)
}
#[tauri::command]
pub async fn list_bluetooth_devices() -> Result<Vec<BluetoothDevice>, String> {
let connection = Connection::system()
.await
.map_err(|e| format!("D-Bus system connection failed: {}", e))?;
dbus_service::list_devices(&connection)
.await
.map_err(|e| e.to_string())
}
|
Validación de Entrada
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
#[tauri::command]
pub fn process_data(
name: String,
age: u32,
email: String,
) -> Result<ProcessedData, String> {
// Validar nombre
if name.is_empty() || name.len() > 100 {
return Err("Invalid name length".to_string());
}
// Validar edad
if age < 18 || age > 120 {
return Err("Invalid age".to_string());
}
// Validar email
if !email.contains('@') {
return Err("Invalid email".to_string());
}
Ok(ProcessedData {
name,
age,
email,
})
}
|
Comandos Relacionados con Hardware
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
// Control de brillo
#[tauri::command]
pub fn set_brightness(level: u32) -> Result<(), String> {
if level > 100 {
return Err("Brightness must be 0-100".to_string());
}
// Usar D-Bus o archivo sysfs
std::fs::write(
"/sys/class/backlight/intel_backlight/brightness",
format!("{}", level * 255 / 100)
).map_err(|e| e.to_string())
}
// Control de Bluetooth
#[tauri::command]
pub async fn scan_bluetooth_devices() -> Result<Vec<Device>, String> {
// Usar BlueZ D-Bus API
Ok(vec![])
}
// Control de red
#[tauri::command]
pub async fn get_wifi_networks() -> Result<Vec<WiFiNetwork>, String> {
// Usar NetworkManager D-Bus API
Ok(vec![])
}
|
Permisología (Tauri Capabilities)
En src-tauri/capabilities/default.json:
1
2
3
4
5
6
7
8
|
{
"permission": [
"core:window:allow-create",
"core:fs:allow-read-file",
"core:fs:allow-write-file",
"core:shell:allow-execute"
]
}
|
Restringir comandos:
1
2
3
4
5
6
|
{
"commands": {
"allow": ["safe_command"],
"deny": ["dangerous_command"]
}
}
|
Testing de Comandos
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_set_volume_valid() {
let result = set_volume(50);
assert!(result.is_ok());
}
#[test]
fn test_set_volume_invalid() {
let result = set_volume(150);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Volume must be 0-100");
}
#[tokio::test]
async fn test_async_command() {
let devices = load_devices().await;
assert!(devices.is_ok());
}
}
|
Mejores Prácticas
✅ Haz:
- Valida entrada siempre
- Retorna errores explícitos
- Documenta comandos
- Usa tipos en lugar de strings
- Maneja timeouts en async
- Limpia recursos
❌ No hagas:
- Confíes en entrada del frontend
- Ignores errores
- Bloquees el hilo principal
- Uses unwrap en producción
- Hagas operaciones muy largas sin feedback
- Guardes secretos en el código
Checklist de Comando