Rust · 18503 bytes Raw Blame History
1 //! Tauri IPC commands
2
3 use crate::state::AppState;
4 use serde::{Deserialize, Serialize};
5 use std::sync::Arc;
6 use tauri::State;
7 use tokio::sync::Mutex;
8 use wanda_core::{
9 config::WandaConfig,
10 launcher::{GameLauncher, LaunchConfig},
11 prefix::{PrefixHealth, PrefixIssue, PrefixManager},
12 steam::{ProtonCompatibility, ProtonManager, SteamInstallation},
13 wemod::{WemodDownloader, WemodInstaller},
14 };
15
16 // ============================================================================
17 // Data Transfer Objects
18 // ============================================================================
19
20 #[derive(Debug, Serialize, Deserialize)]
21 pub struct GameInfo {
22 pub app_id: u32,
23 pub name: String,
24 pub size: String,
25 pub uses_proton: bool,
26 pub install_path: String,
27 }
28
29 #[derive(Debug, Serialize, Deserialize)]
30 pub struct PrefixInfo {
31 pub name: String,
32 pub path: String,
33 pub wemod_installed: bool,
34 pub wemod_version: Option<String>,
35 pub proton_version: Option<String>,
36 pub health: String,
37 pub issues: Vec<String>,
38 }
39
40 #[derive(Debug, Serialize, Deserialize)]
41 pub struct ProtonInfo {
42 pub name: String,
43 pub path: String,
44 pub compatibility: String,
45 pub is_ge: bool,
46 pub is_recommended: bool,
47 }
48
49 #[derive(Debug, Serialize, Deserialize)]
50 pub struct WemodStatus {
51 pub installed: bool,
52 pub version: Option<String>,
53 pub update_available: bool,
54 pub latest_version: Option<String>,
55 }
56
57 #[derive(Debug, Serialize, Deserialize)]
58 pub struct InitStatus {
59 pub initialized: bool,
60 pub steam_found: bool,
61 pub proton_found: bool,
62 pub prefix_exists: bool,
63 pub wemod_installed: bool,
64 }
65
66 #[derive(Debug, Serialize, Deserialize)]
67 pub struct DoctorReport {
68 pub steam_ok: bool,
69 pub steam_path: Option<String>,
70 pub proton_ok: bool,
71 pub proton_count: usize,
72 pub prefix_ok: bool,
73 pub wemod_ok: bool,
74 pub issues: Vec<String>,
75 }
76
77 #[derive(Debug, Serialize, Deserialize)]
78 pub struct ConfigDto {
79 pub steam_path: Option<String>,
80 pub scan_flatpak: bool,
81 pub preferred_proton: Option<String>,
82 pub auto_update_wemod: bool,
83 }
84
85 // ============================================================================
86 // Game Commands
87 // ============================================================================
88
89 #[tauri::command]
90 pub async fn get_games(state: State<'_, Arc<Mutex<AppState>>>) -> Result<Vec<GameInfo>, String> {
91 let mut state = state.lock().await;
92 state.ensure_loaded()?;
93
94 let steam = state.steam.as_ref().ok_or("Steam not loaded")?;
95
96 let games: Vec<GameInfo> = steam
97 .get_all_games()
98 .iter()
99 .filter(|g| g.uses_proton())
100 .map(|g| GameInfo {
101 app_id: g.app_id,
102 name: g.name.clone(),
103 size: g.size_human(),
104 uses_proton: g.uses_proton(),
105 install_path: g.install_path.to_string_lossy().to_string(),
106 })
107 .collect();
108
109 Ok(games)
110 }
111
112 #[tauri::command]
113 pub async fn get_game(
114 app_id: u32,
115 state: State<'_, Arc<Mutex<AppState>>>,
116 ) -> Result<GameInfo, String> {
117 let mut state = state.lock().await;
118 state.ensure_loaded()?;
119
120 let steam = state.steam.as_ref().ok_or("Steam not loaded")?;
121 let game = steam
122 .find_game(app_id)
123 .ok_or_else(|| format!("Game {} not found", app_id))?;
124
125 Ok(GameInfo {
126 app_id: game.app_id,
127 name: game.name.clone(),
128 size: game.size_human(),
129 uses_proton: game.uses_proton(),
130 install_path: game.install_path.to_string_lossy().to_string(),
131 })
132 }
133
134 #[tauri::command]
135 pub async fn launch_game(
136 app_id: u32,
137 with_wemod: bool,
138 state: State<'_, Arc<Mutex<AppState>>>,
139 ) -> Result<(), String> {
140 let mut state = state.lock().await;
141 state.ensure_loaded()?;
142
143 let steam = state.steam.as_ref().ok_or("Steam not loaded")?;
144 let config = state.config.as_ref().ok_or("Config not loaded")?;
145 let proton_mgr = state.proton.as_ref().ok_or("Proton not loaded")?;
146 let prefix_mgr = state.prefix_manager.as_ref().ok_or("Prefix manager not loaded")?;
147
148 let proton = proton_mgr
149 .get_preferred(config)
150 .map_err(|e| e.to_string())?;
151
152 let prefix = prefix_mgr
153 .get("default")
154 .ok_or("WANDA not initialized")?;
155
156 let launcher = GameLauncher::new(steam, prefix, proton);
157
158 let launch_config = LaunchConfig {
159 app_id,
160 with_wemod,
161 wemod_delay: 3,
162 ..Default::default()
163 };
164
165 launcher.launch(launch_config).await.map_err(|e| e.to_string())?;
166
167 Ok(())
168 }
169
170 // ============================================================================
171 // Prefix Commands
172 // ============================================================================
173
174 #[tauri::command]
175 pub async fn get_prefixes(state: State<'_, Arc<Mutex<AppState>>>) -> Result<Vec<PrefixInfo>, String> {
176 let mut state = state.lock().await;
177 state.ensure_loaded()?;
178
179 let prefix_mgr = state.prefix_manager.as_ref().ok_or("Prefix manager not loaded")?;
180
181 let prefixes: Vec<PrefixInfo> = prefix_mgr
182 .list()
183 .iter()
184 .map(|p| {
185 let health = prefix_mgr.validate(&p.name).unwrap_or(PrefixHealth::NotCreated);
186 let (health_str, issues) = match &health {
187 PrefixHealth::Healthy => ("healthy".to_string(), vec![]),
188 PrefixHealth::NeedsRepair(issues) => (
189 "needs_repair".to_string(),
190 issues.iter().map(|i| i.to_string()).collect(),
191 ),
192 PrefixHealth::Corrupted(reason) => ("corrupted".to_string(), vec![reason.clone()]),
193 PrefixHealth::NotCreated => ("not_created".to_string(), vec![]),
194 };
195
196 PrefixInfo {
197 name: p.name.clone(),
198 path: p.path.to_string_lossy().to_string(),
199 wemod_installed: p.wemod_installed,
200 wemod_version: p.wemod_version.clone(),
201 proton_version: p.proton_version.clone(),
202 health: health_str,
203 issues,
204 }
205 })
206 .collect();
207
208 Ok(prefixes)
209 }
210
211 #[tauri::command]
212 pub async fn get_prefix_health(
213 name: String,
214 state: State<'_, Arc<Mutex<AppState>>>,
215 ) -> Result<PrefixInfo, String> {
216 let mut state = state.lock().await;
217 state.ensure_loaded()?;
218
219 let prefix_mgr = state.prefix_manager.as_ref().ok_or("Prefix manager not loaded")?;
220
221 let prefix = prefix_mgr.get(&name).ok_or("Prefix not found")?;
222 let health = prefix_mgr.validate(&name).map_err(|e| e.to_string())?;
223
224 let (health_str, issues) = match &health {
225 PrefixHealth::Healthy => ("healthy".to_string(), vec![]),
226 PrefixHealth::NeedsRepair(issues) => (
227 "needs_repair".to_string(),
228 issues.iter().map(|i| i.to_string()).collect(),
229 ),
230 PrefixHealth::Corrupted(reason) => ("corrupted".to_string(), vec![reason.clone()]),
231 PrefixHealth::NotCreated => ("not_created".to_string(), vec![]),
232 };
233
234 Ok(PrefixInfo {
235 name: prefix.name.clone(),
236 path: prefix.path.to_string_lossy().to_string(),
237 wemod_installed: prefix.wemod_installed,
238 wemod_version: prefix.wemod_version.clone(),
239 proton_version: prefix.proton_version.clone(),
240 health: health_str,
241 issues,
242 })
243 }
244
245 #[tauri::command]
246 pub async fn repair_prefix(
247 name: String,
248 state: State<'_, Arc<Mutex<AppState>>>,
249 ) -> Result<(), String> {
250 let mut state = state.lock().await;
251 state.ensure_loaded()?;
252
253 let config = state.config.as_ref().ok_or("Config not loaded")?;
254 let proton_mgr = state.proton.as_ref().ok_or("Proton not loaded")?;
255 let prefix_mgr = state.prefix_manager.as_mut().ok_or("Prefix manager not loaded")?;
256
257 let proton = proton_mgr
258 .get_preferred(config)
259 .map_err(|e| e.to_string())?;
260
261 prefix_mgr.repair(&name, proton).await.map_err(|e| e.to_string())?;
262
263 Ok(())
264 }
265
266 // ============================================================================
267 // Initialization Commands
268 // ============================================================================
269
270 #[tauri::command]
271 pub async fn get_init_status(state: State<'_, Arc<Mutex<AppState>>>) -> Result<InitStatus, String> {
272 let mut state = state.lock().await;
273
274 // Try to load, but don't fail if we can't
275 let _ = state.load();
276
277 let steam_found = state.steam.is_some();
278 let proton_found = state.proton.as_ref().map(|p| !p.versions.is_empty()).unwrap_or(false);
279 let prefix_exists = state
280 .prefix_manager
281 .as_ref()
282 .map(|pm| pm.get("default").is_some())
283 .unwrap_or(false);
284 let wemod_installed = state
285 .prefix_manager
286 .as_ref()
287 .and_then(|pm| pm.get("default"))
288 .map(|p| p.wemod_installed)
289 .unwrap_or(false);
290
291 Ok(InitStatus {
292 initialized: wemod_installed,
293 steam_found,
294 proton_found,
295 prefix_exists,
296 wemod_installed,
297 })
298 }
299
300 #[tauri::command]
301 pub async fn init_wanda(state: State<'_, Arc<Mutex<AppState>>>) -> Result<(), String> {
302 let mut state = state.lock().await;
303
304 // Load config
305 let config = WandaConfig::load().map_err(|e| e.to_string())?;
306
307 // Discover Steam
308 let steam = SteamInstallation::discover(&config).map_err(|e| e.to_string())?;
309
310 // Discover Proton
311 let proton_mgr = ProtonManager::discover(&steam, &config).map_err(|e| e.to_string())?;
312 let proton = proton_mgr.get_preferred(&config).map_err(|e| e.to_string())?;
313
314 // Create prefix manager and default prefix
315 let mut prefix_mgr = PrefixManager::new(&config);
316 prefix_mgr.load().map_err(|e| e.to_string())?;
317
318 if prefix_mgr.get("default").is_none() {
319 prefix_mgr.create("default", proton).await.map_err(|e| e.to_string())?;
320 }
321
322 // Download and install WeMod
323 let downloader = WemodDownloader::new(&config);
324 let release = downloader.get_latest().await.map_err(|e| e.to_string())?;
325 let installer_path = downloader
326 .download(&release, |_, _| {})
327 .await
328 .map_err(|e| e.to_string())?;
329
330 let prefix = prefix_mgr.get("default").ok_or("Prefix not created")?;
331 let installer = WemodInstaller::new(prefix, proton);
332 installer.install(&installer_path).await.map_err(|e| e.to_string())?;
333
334 // Update prefix metadata
335 let version = release.version.clone();
336 prefix_mgr.update_metadata("default", |p| {
337 p.wemod_installed = true;
338 p.wemod_version = version;
339 }).map_err(|e| e.to_string())?;
340
341 // Save config
342 let mut config = config;
343 config.proton.preferred_version = Some(proton.name.clone());
344 config.save().map_err(|e| e.to_string())?;
345
346 // Reload state
347 state.load()?;
348
349 Ok(())
350 }
351
352 // ============================================================================
353 // Config Commands
354 // ============================================================================
355
356 #[tauri::command]
357 pub async fn get_config(state: State<'_, Arc<Mutex<AppState>>>) -> Result<ConfigDto, String> {
358 let mut state = state.lock().await;
359 state.ensure_loaded()?;
360
361 let config = state.config.as_ref().ok_or("Config not loaded")?;
362
363 Ok(ConfigDto {
364 steam_path: config.steam.install_path.as_ref().map(|p| p.to_string_lossy().to_string()),
365 scan_flatpak: config.steam.scan_flatpak,
366 preferred_proton: config.proton.preferred_version.clone(),
367 auto_update_wemod: config.wemod.auto_update,
368 })
369 }
370
371 #[tauri::command]
372 pub async fn update_config(
373 config_dto: ConfigDto,
374 state: State<'_, Arc<Mutex<AppState>>>,
375 ) -> Result<(), String> {
376 let mut state = state.lock().await;
377 state.ensure_loaded()?;
378
379 let config = state.config.as_mut().ok_or("Config not loaded")?;
380
381 config.steam.install_path = config_dto.steam_path.map(std::path::PathBuf::from);
382 config.steam.scan_flatpak = config_dto.scan_flatpak;
383 config.proton.preferred_version = config_dto.preferred_proton;
384 config.wemod.auto_update = config_dto.auto_update_wemod;
385
386 config.save().map_err(|e| e.to_string())?;
387
388 // Reload state to reflect changes
389 state.load()?;
390
391 Ok(())
392 }
393
394 // ============================================================================
395 // WeMod Commands
396 // ============================================================================
397
398 #[tauri::command]
399 pub async fn get_wemod_status(state: State<'_, Arc<Mutex<AppState>>>) -> Result<WemodStatus, String> {
400 let mut state = state.lock().await;
401 state.ensure_loaded()?;
402
403 let config = state.config.as_ref().ok_or("Config not loaded")?;
404 let prefix_mgr = state.prefix_manager.as_ref().ok_or("Prefix manager not loaded")?;
405
406 let prefix = prefix_mgr.get("default");
407 let installed = prefix.map(|p| p.wemod_installed).unwrap_or(false);
408 let version = prefix.and_then(|p| p.wemod_version.clone());
409
410 // Check for updates
411 let downloader = WemodDownloader::new(config);
412 let (update_available, latest_version) = match downloader.get_latest().await {
413 Ok(release) => {
414 let latest = release.version.clone();
415 let update = match (&version, &latest) {
416 (Some(current), Some(new)) => current != new,
417 _ => false,
418 };
419 (update, latest)
420 }
421 Err(_) => (false, None),
422 };
423
424 Ok(WemodStatus {
425 installed,
426 version,
427 update_available,
428 latest_version,
429 })
430 }
431
432 #[tauri::command]
433 pub async fn update_wemod(state: State<'_, Arc<Mutex<AppState>>>) -> Result<(), String> {
434 let mut state = state.lock().await;
435 state.ensure_loaded()?;
436
437 let config = state.config.as_ref().ok_or("Config not loaded")?;
438 let proton_mgr = state.proton.as_ref().ok_or("Proton not loaded")?;
439 let prefix_mgr = state.prefix_manager.as_mut().ok_or("Prefix manager not loaded")?;
440
441 let proton = proton_mgr.get_preferred(config).map_err(|e| e.to_string())?;
442 let prefix = prefix_mgr.get("default").ok_or("WANDA not initialized")?;
443
444 let downloader = WemodDownloader::new(config);
445 let release = downloader.get_latest().await.map_err(|e| e.to_string())?;
446 let installer_path = downloader
447 .download(&release, |_, _| {})
448 .await
449 .map_err(|e| e.to_string())?;
450
451 let installer = WemodInstaller::new(prefix, proton);
452 installer.install(&installer_path).await.map_err(|e| e.to_string())?;
453
454 let version = release.version.clone();
455 prefix_mgr.update_metadata("default", |p| {
456 p.wemod_version = version;
457 }).map_err(|e| e.to_string())?;
458
459 Ok(())
460 }
461
462 // ============================================================================
463 // Proton Commands
464 // ============================================================================
465
466 #[tauri::command]
467 pub async fn get_proton_versions(
468 state: State<'_, Arc<Mutex<AppState>>>,
469 ) -> Result<Vec<ProtonInfo>, String> {
470 let mut state = state.lock().await;
471 state.ensure_loaded()?;
472
473 let config = state.config.as_ref().ok_or("Config not loaded")?;
474 let proton_mgr = state.proton.as_ref().ok_or("Proton not loaded")?;
475
476 let recommended = proton_mgr.get_preferred(config).ok();
477
478 let versions: Vec<ProtonInfo> = proton_mgr
479 .versions
480 .iter()
481 .map(|v| ProtonInfo {
482 name: v.name.clone(),
483 path: v.path.to_string_lossy().to_string(),
484 compatibility: match v.compatibility {
485 ProtonCompatibility::Recommended => "recommended".to_string(),
486 ProtonCompatibility::Supported => "supported".to_string(),
487 ProtonCompatibility::Experimental => "experimental".to_string(),
488 ProtonCompatibility::Unsupported => "unsupported".to_string(),
489 },
490 is_ge: v.is_ge,
491 is_recommended: recommended.map(|r| r.name == v.name).unwrap_or(false),
492 })
493 .collect();
494
495 Ok(versions)
496 }
497
498 // ============================================================================
499 // Doctor Commands
500 // ============================================================================
501
502 #[tauri::command]
503 pub async fn run_doctor(state: State<'_, Arc<Mutex<AppState>>>) -> Result<DoctorReport, String> {
504 let mut state = state.lock().await;
505
506 let mut issues = Vec::new();
507
508 // Try to load config
509 let config = match WandaConfig::load() {
510 Ok(c) => Some(c),
511 Err(e) => {
512 issues.push(format!("Config error: {}", e));
513 None
514 }
515 };
516
517 // Check Steam
518 let (steam_ok, steam_path) = if let Some(ref cfg) = config {
519 match SteamInstallation::discover(cfg) {
520 Ok(steam) => (true, Some(steam.root_path.to_string_lossy().to_string())),
521 Err(e) => {
522 issues.push(format!("Steam not found: {}", e));
523 (false, None)
524 }
525 }
526 } else {
527 (false, None)
528 };
529
530 // Check Proton
531 let (proton_ok, proton_count) = if let (Some(ref cfg), true) = (&config, steam_ok) {
532 let steam = SteamInstallation::discover(cfg).unwrap();
533 match ProtonManager::discover(&steam, cfg) {
534 Ok(pm) if !pm.versions.is_empty() => (true, pm.versions.len()),
535 Ok(_) => {
536 issues.push("No Proton versions found".to_string());
537 (false, 0)
538 }
539 Err(e) => {
540 issues.push(format!("Proton error: {}", e));
541 (false, 0)
542 }
543 }
544 } else {
545 (false, 0)
546 };
547
548 // Check prefix
549 let (prefix_ok, wemod_ok) = if let Some(ref cfg) = config {
550 let mut pm = PrefixManager::new(cfg);
551 let _ = pm.load();
552 if let Some(prefix) = pm.get("default") {
553 let health_ok = matches!(pm.validate("default"), Ok(PrefixHealth::Healthy));
554 if !health_ok {
555 issues.push("Prefix needs repair".to_string());
556 }
557 (true, prefix.wemod_installed)
558 } else {
559 issues.push("WANDA not initialized".to_string());
560 (false, false)
561 }
562 } else {
563 (false, false)
564 };
565
566 if !wemod_ok && prefix_ok {
567 issues.push("WeMod not installed".to_string());
568 }
569
570 // Update state if load was successful
571 if config.is_some() {
572 let _ = state.load();
573 }
574
575 Ok(DoctorReport {
576 steam_ok,
577 steam_path,
578 proton_ok,
579 proton_count,
580 prefix_ok,
581 wemod_ok,
582 issues,
583 })
584 }
585