formatting

This commit is contained in:
Tickbase
2025-05-17 22:49:09 +02:00
parent ecd05f1980
commit 76bfea819b
46 changed files with 2905 additions and 2743 deletions

View File

@@ -8,9 +8,6 @@ repository = ""
edition = "2021"
rust-version = "1.77.2"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
tauri-build = { version = "2.2.0", features = [] }
@@ -37,6 +34,4 @@ num_cpus = "1.16.0"
futures = "0.3.31"
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]

View File

@@ -1,3 +1,3 @@
fn main() {
tauri_build::build()
tauri_build::build()
}

View File

@@ -2,10 +2,6 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "enables the default permissions",
"windows": [
"main"
],
"permissions": [
"core:default"
]
"windows": ["main"],
"permissions": ["core:default"]
}

View File

@@ -1,13 +1,11 @@
// src/cache.rs
use serde::{Serialize, Deserialize};
use crate::dlc_manager::DlcInfoWithState;
use log::{info, warn};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::path::{PathBuf};
use std::fs;
use std::io;
use std::time::{SystemTime};
use log::{info, warn};
use crate::dlc_manager::DlcInfoWithState;
use std::path::PathBuf;
use std::time::SystemTime;
// Cache entry with timestamp for expiration
#[derive(Serialize, Deserialize)]
@@ -20,14 +18,14 @@ struct CacheEntry<T> {
fn get_cache_dir() -> io::Result<PathBuf> {
let xdg_dirs = xdg::BaseDirectories::with_prefix("creamlinux")
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let cache_dir = xdg_dirs.get_cache_home();
// Make sure the cache directory exists
if !cache_dir.exists() {
fs::create_dir_all(&cache_dir)?;
}
Ok(cache_dir)
}
@@ -38,26 +36,26 @@ where
{
let cache_dir = get_cache_dir()?;
let cache_file = cache_dir.join(format!("{}.cache", key));
// Get current timestamp
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
// Create a JSON object with timestamp and data directly
let json_data = json!({
"timestamp": now,
"data": data // No clone needed here
});
// Serialize and write to file
let serialized = serde_json::to_string(&json_data)
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
let serialized =
serde_json::to_string(&json_data).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
fs::write(cache_file, serialized)?;
info!("Saved cache for key: {}", key);
Ok(())
}
@@ -73,14 +71,14 @@ where
return None;
}
};
let cache_file = cache_dir.join(format!("{}.cache", key));
// Check if cache file exists
if !cache_file.exists() {
return None;
}
// Read and deserialize
let cached_data = match fs::read_to_string(&cache_file) {
Ok(data) => data,
@@ -89,54 +87,58 @@ where
return None;
}
};
// Parse the JSON
let json_value: serde_json::Value = match serde_json::from_str(&cached_data) {
Ok(v) => v,
Err(e) => {
warn!("Failed to parse cache file {}: {}", cache_file.display(), e);
return None;
}
};
// Extract timestamp
let timestamp = match json_value.get("timestamp").and_then(|v| v.as_u64()) {
Some(ts) => ts,
None => {
warn!("Invalid timestamp in cache file {}", cache_file.display());
return None;
}
};
// Check expiration
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let age_hours = (now - timestamp) / 3600;
if age_hours > ttl_hours {
info!("Cache for key {} is expired ({} hours old)", key, age_hours);
return None;
}
// Extract data
let data: T = match serde_json::from_value(json_value["data"].clone()) {
Ok(d) => d,
Err(e) => {
warn!("Failed to parse data in cache file {}: {}", cache_file.display(), e);
return None;
}
};
info!("Using cache for key {} ({} hours old)", key, age_hours);
Some(data)
Ok(v) => v,
Err(e) => {
warn!("Failed to parse cache file {}: {}", cache_file.display(), e);
return None;
}
};
// Extract timestamp
let timestamp = match json_value.get("timestamp").and_then(|v| v.as_u64()) {
Some(ts) => ts,
None => {
warn!("Invalid timestamp in cache file {}", cache_file.display());
return None;
}
};
// Check expiration
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let age_hours = (now - timestamp) / 3600;
if age_hours > ttl_hours {
info!("Cache for key {} is expired ({} hours old)", key, age_hours);
return None;
}
// Extract data
let data: T = match serde_json::from_value(json_value["data"].clone()) {
Ok(d) => d,
Err(e) => {
warn!(
"Failed to parse data in cache file {}: {}",
cache_file.display(),
e
);
return None;
}
};
info!("Using cache for key {} ({} hours old)", key, age_hours);
Some(data)
}
// Cache game scanning results
pub fn cache_games(games: &[crate::installer::Game]) -> io::Result<()> {
save_to_cache("games", games, 24) // Cache games for 24 hours
save_to_cache("games", games, 24) // Cache games for 24 hours
}
// Load cached game scanning results
@@ -146,7 +148,7 @@ pub fn load_cached_games() -> Option<Vec<crate::installer::Game>> {
// Cache DLC list for a game
pub fn cache_dlcs(game_id: &str, dlcs: &[DlcInfoWithState]) -> io::Result<()> {
save_to_cache(&format!("dlc_{}", game_id), dlcs, 168) // Cache DLCs for 7 days (168 hours)
save_to_cache(&format!("dlc_{}", game_id), dlcs, 168) // Cache DLCs for 7 days (168 hours)
}
// Load cached DLC list
@@ -157,11 +159,11 @@ pub fn load_cached_dlcs(game_id: &str) -> Option<Vec<DlcInfoWithState>> {
// Clear all caches
pub fn clear_all_caches() -> io::Result<()> {
let cache_dir = get_cache_dir()?;
for entry in fs::read_dir(cache_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() && path.extension().map_or(false, |ext| ext == "cache") {
if let Err(e) = fs::remove_file(&path) {
warn!("Failed to remove cache file {}: {}", path.display(), e);
@@ -170,7 +172,7 @@ pub fn clear_all_caches() -> io::Result<()> {
}
}
}
info!("All caches cleared");
Ok(())
}
}

View File

@@ -1,12 +1,11 @@
// src/dlc_manager.rs
use serde::{Serialize, Deserialize};
use log::{error, info};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::Path;
use log::{info, error};
use std::collections::{HashMap, HashSet};
use tauri::Manager;
/// More detailed DLC information with enabled state
// More detailed DLC information with enabled state
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct DlcInfoWithState {
pub appid: String,
@@ -14,39 +13,42 @@ pub struct DlcInfoWithState {
pub enabled: bool,
}
/// Parse the cream_api.ini file to extract both enabled and disabled DLCs
// Parse the cream_api.ini file to extract both enabled and disabled DLCs
pub fn get_enabled_dlcs(game_path: &str) -> Result<Vec<String>, String> {
info!("Reading enabled DLCs from {}", game_path);
let cream_api_path = Path::new(game_path).join("cream_api.ini");
if !cream_api_path.exists() {
return Err(format!("cream_api.ini not found at {}", cream_api_path.display()));
return Err(format!(
"cream_api.ini not found at {}",
cream_api_path.display()
));
}
let contents = match fs::read_to_string(&cream_api_path) {
Ok(c) => c,
Err(e) => return Err(format!("Failed to read cream_api.ini: {}", e))
Err(e) => return Err(format!("Failed to read cream_api.ini: {}", e)),
};
// Extract DLCs - they are in the [dlc] section with format "appid = name"
// Extract DLCs
let mut in_dlc_section = false;
let mut enabled_dlcs = Vec::new();
for line in contents.lines() {
let trimmed = line.trim();
// Check if we're in the DLC section
if trimmed == "[dlc]" {
in_dlc_section = true;
continue;
}
// Check if we're leaving the DLC section (another section begins)
// Check if we're leaving the DLC section
if in_dlc_section && trimmed.starts_with('[') && trimmed.ends_with(']') {
in_dlc_section = false;
continue;
}
// Skip empty lines and non-DLC comments
if in_dlc_section && !trimmed.is_empty() && !trimmed.starts_with(';') {
// Extract the DLC app ID
@@ -59,44 +61,47 @@ pub fn get_enabled_dlcs(game_path: &str) -> Result<Vec<String>, String> {
}
}
}
info!("Found {} enabled DLCs", enabled_dlcs.len());
Ok(enabled_dlcs)
}
/// Get all DLCs (both enabled and disabled) from cream_api.ini
// Get all DLCs (both enabled and disabled) from cream_api.ini
pub fn get_all_dlcs(game_path: &str) -> Result<Vec<DlcInfoWithState>, String> {
info!("Reading all DLCs from {}", game_path);
let cream_api_path = Path::new(game_path).join("cream_api.ini");
if !cream_api_path.exists() {
return Err(format!("cream_api.ini not found at {}", cream_api_path.display()));
return Err(format!(
"cream_api.ini not found at {}",
cream_api_path.display()
));
}
let contents = match fs::read_to_string(&cream_api_path) {
Ok(c) => c,
Err(e) => return Err(format!("Failed to read cream_api.ini: {}", e))
Err(e) => return Err(format!("Failed to read cream_api.ini: {}", e)),
};
// Extract DLCs - both enabled and disabled
// Extract DLCs
let mut in_dlc_section = false;
let mut all_dlcs = Vec::new();
for line in contents.lines() {
let trimmed = line.trim();
// Check if we're in the DLC section
if trimmed == "[dlc]" {
in_dlc_section = true;
continue;
}
// Check if we're leaving the DLC section (another section begins)
// Check if we're leaving the DLC section
if in_dlc_section && trimmed.starts_with('[') && trimmed.ends_with(']') {
in_dlc_section = false;
continue;
}
// Process DLC entries (both enabled and commented/disabled)
if in_dlc_section && !trimmed.is_empty() && !trimmed.starts_with(';') {
let is_commented = trimmed.starts_with("#");
@@ -105,12 +110,12 @@ pub fn get_all_dlcs(game_path: &str) -> Result<Vec<DlcInfoWithState>, String> {
} else {
trimmed
};
let parts: Vec<&str> = actual_line.splitn(2, '=').collect();
if parts.len() == 2 {
let appid = parts[0].trim();
let name = parts[1].trim();
all_dlcs.push(DlcInfoWithState {
appid: appid.to_string(),
name: name.to_string().trim_matches('"').to_string(),
@@ -119,56 +124,65 @@ pub fn get_all_dlcs(game_path: &str) -> Result<Vec<DlcInfoWithState>, String> {
}
}
}
info!("Found {} total DLCs ({} enabled, {} disabled)",
all_dlcs.len(),
all_dlcs.iter().filter(|d| d.enabled).count(),
all_dlcs.iter().filter(|d| !d.enabled).count());
info!(
"Found {} total DLCs ({} enabled, {} disabled)",
all_dlcs.len(),
all_dlcs.iter().filter(|d| d.enabled).count(),
all_dlcs.iter().filter(|d| !d.enabled).count()
);
Ok(all_dlcs)
}
/// Update the cream_api.ini file with the user's DLC selections
pub fn update_dlc_configuration(game_path: &str, dlcs: Vec<DlcInfoWithState>) -> Result<(), String> {
// Update the cream_api.ini file with the user's DLC selections
pub fn update_dlc_configuration(
game_path: &str,
dlcs: Vec<DlcInfoWithState>,
) -> Result<(), String> {
info!("Updating DLC configuration for {}", game_path);
let cream_api_path = Path::new(game_path).join("cream_api.ini");
if !cream_api_path.exists() {
return Err(format!("cream_api.ini not found at {}", cream_api_path.display()));
return Err(format!(
"cream_api.ini not found at {}",
cream_api_path.display()
));
}
// Read the current file contents
let current_contents = match fs::read_to_string(&cream_api_path) {
Ok(c) => c,
Err(e) => return Err(format!("Failed to read cream_api.ini: {}", e))
Err(e) => return Err(format!("Failed to read cream_api.ini: {}", e)),
};
// Create a mapping of DLC appid to its state for easy lookup
let dlc_states: HashMap<String, (bool, String)> = dlcs.iter()
let dlc_states: HashMap<String, (bool, String)> = dlcs
.iter()
.map(|dlc| (dlc.appid.clone(), (dlc.enabled, dlc.name.clone())))
.collect();
// Keep track of processed DLCs to avoid duplicates
let mut processed_dlcs = HashSet::new();
// Process the file line by line to retain most of the original structure
let mut new_contents = Vec::new();
let mut in_dlc_section = false;
for line in current_contents.lines() {
let trimmed = line.trim();
// Add section markers directly
if trimmed == "[dlc]" {
in_dlc_section = true;
new_contents.push(line.to_string());
continue;
}
// Check if we're leaving the DLC section (another section begins)
// Check if we're leaving the DLC section
if in_dlc_section && trimmed.starts_with('[') && trimmed.ends_with(']') {
in_dlc_section = false;
// Before leaving the DLC section, add any DLCs that weren't processed yet
for (appid, (enabled, name)) in &dlc_states {
if !processed_dlcs.contains(appid) {
@@ -179,21 +193,21 @@ pub fn update_dlc_configuration(game_path: &str, dlcs: Vec<DlcInfoWithState>) ->
}
}
}
// Now add the section marker
new_contents.push(line.to_string());
continue;
}
if in_dlc_section && !trimmed.is_empty() {
let is_comment_line = trimmed.starts_with(';');
// If it's a regular comment line (not a DLC), keep it as is
if is_comment_line {
new_contents.push(line.to_string());
continue;
}
// Check if it's a commented-out DLC line or a regular DLC line
let is_commented = trimmed.starts_with("#");
let actual_line = if is_commented {
@@ -201,13 +215,13 @@ pub fn update_dlc_configuration(game_path: &str, dlcs: Vec<DlcInfoWithState>) ->
} else {
trimmed
};
// Extract appid and name
let parts: Vec<&str> = actual_line.splitn(2, '=').collect();
if parts.len() == 2 {
let appid = parts[0].trim();
let name = parts[1].trim();
// Check if this DLC exists in our updated list
if let Some((enabled, _)) = dlc_states.get(appid) {
// Add the DLC with its updated state
@@ -218,19 +232,19 @@ pub fn update_dlc_configuration(game_path: &str, dlcs: Vec<DlcInfoWithState>) ->
}
processed_dlcs.insert(appid.to_string());
} else {
// Not in our list - keep the original line
// Not in our list keep the original line
new_contents.push(line.to_string());
}
} else {
// Invalid format or not a DLC line - keep as is
// Invalid format or not a DLC line keep as is
new_contents.push(line.to_string());
}
} else if !in_dlc_section || trimmed.is_empty() {
// Not a DLC line or empty line - keep as is
// Not a DLC line or empty line keep as is
new_contents.push(line.to_string());
}
}
// If we never left the DLC section, make sure we add any unprocessed DLCs
if in_dlc_section {
for (appid, (enabled, name)) in &dlc_states {
@@ -243,13 +257,16 @@ pub fn update_dlc_configuration(game_path: &str, dlcs: Vec<DlcInfoWithState>) ->
}
}
}
// Write the updated file
match fs::write(&cream_api_path, new_contents.join("\n")) {
Ok(_) => {
info!("Successfully updated DLC configuration at {}", cream_api_path.display());
info!(
"Successfully updated DLC configuration at {}",
cream_api_path.display()
);
Ok(())
},
}
Err(e) => {
error!("Failed to write updated cream_api.ini: {}", e);
Err(format!("Failed to write updated cream_api.ini: {}", e))
@@ -257,7 +274,7 @@ pub fn update_dlc_configuration(game_path: &str, dlcs: Vec<DlcInfoWithState>) ->
}
}
/// Get app ID from game path by reading cream_api.ini
// Get app ID from game path by reading cream_api.ini
#[allow(dead_code)]
fn extract_app_id_from_config(game_path: &str) -> Option<String> {
if let Ok(contents) = fs::read_to_string(Path::new(game_path).join("cream_api.ini")) {
@@ -269,71 +286,83 @@ fn extract_app_id_from_config(game_path: &str) -> Option<String> {
None
}
/// Create a custom installation with selected DLCs
// Create a custom installation with selected DLCs
pub async fn install_cream_with_dlcs(
game_id: String,
app_handle: tauri::AppHandle,
selected_dlcs: Vec<DlcInfoWithState>
game_id: String,
app_handle: tauri::AppHandle,
selected_dlcs: Vec<DlcInfoWithState>,
) -> Result<(), String> {
use crate::AppState;
// Count enabled DLCs for logging
let enabled_dlc_count = selected_dlcs.iter().filter(|dlc| dlc.enabled).count();
info!("Starting installation of CreamLinux with {} selected DLCs", enabled_dlc_count);
// Get the game from state
let game = {
let state = app_handle.state::<AppState>();
let games = state.games.lock();
match games.get(&game_id) {
Some(g) => g.clone(),
None => return Err(format!("Game with ID {} not found", game_id))
}
};
info!("Installing CreamLinux for game: {} ({})", game.title, game_id);
// Install CreamLinux first - but provide the DLCs directly instead of fetching them again
use crate::installer::install_creamlinux_with_dlcs;
// Convert DlcInfoWithState to installer::DlcInfo for those that are enabled
let enabled_dlcs = selected_dlcs.iter()
.filter(|dlc| dlc.enabled)
.map(|dlc| crate::installer::DlcInfo {
appid: dlc.appid.clone(),
name: dlc.name.clone(),
})
.collect::<Vec<_>>();
let app_handle_clone = app_handle.clone();
let game_title = game.title.clone();
// Use direct installation with provided DLCs instead of re-fetching
match install_creamlinux_with_dlcs(
&game.path,
&game_id,
enabled_dlcs,
move |progress, message| {
// Emit progress updates during installation
use crate::installer::emit_progress;
emit_progress(
&app_handle_clone,
&format!("Installing CreamLinux for {}", game_title),
message,
progress * 100.0, // Scale progress from 0 to 100%
false,
false,
None
);
}
).await {
Ok(_) => {
info!("CreamLinux installation completed successfully for game: {}", game.title);
Ok(())
},
Err(e) => {
error!("Failed to install CreamLinux: {}", e);
Err(format!("Failed to install CreamLinux: {}", e))
}
}
}
use crate::AppState;
// Count enabled DLCs for logging
let enabled_dlc_count = selected_dlcs.iter().filter(|dlc| dlc.enabled).count();
info!(
"Starting installation of CreamLinux with {} selected DLCs",
enabled_dlc_count
);
// Get the game from state
let game = {
let state = app_handle.state::<AppState>();
let games = state.games.lock();
match games.get(&game_id) {
Some(g) => g.clone(),
None => return Err(format!("Game with ID {} not found", game_id)),
}
};
info!(
"Installing CreamLinux for game: {} ({})",
game.title, game_id
);
// Install CreamLinux first - but provide the DLCs directly instead of fetching them again
use crate::installer::install_creamlinux_with_dlcs;
// Convert DlcInfoWithState to installer::DlcInfo for those that are enabled
let enabled_dlcs = selected_dlcs
.iter()
.filter(|dlc| dlc.enabled)
.map(|dlc| crate::installer::DlcInfo {
appid: dlc.appid.clone(),
name: dlc.name.clone(),
})
.collect::<Vec<_>>();
let app_handle_clone = app_handle.clone();
let game_title = game.title.clone();
// Use direct installation with provided DLCs instead of re-fetching
match install_creamlinux_with_dlcs(
&game.path,
&game_id,
enabled_dlcs,
move |progress, message| {
// Emit progress updates during installation
use crate::installer::emit_progress;
emit_progress(
&app_handle_clone,
&format!("Installing CreamLinux for {}", game_title),
message,
progress * 100.0, // Scale progress from 0 to 100%
false,
false,
None,
);
},
)
.await
{
Ok(_) => {
info!(
"CreamLinux installation completed successfully for game: {}",
game.title
);
Ok(())
}
Err(e) => {
error!("Failed to install CreamLinux: {}", e);
Err(format!("Failed to install CreamLinux: {}", e))
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,100 +1,130 @@
// src/main.rs
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
mod searcher;
mod installer;
mod cache;
mod dlc_manager;
mod cache; // Keep the module for now, but we won't use its functionality
mod installer;
mod searcher; // Keep the module for now
use serde::{Serialize, Deserialize};
use std::collections::HashMap;
use parking_lot::Mutex;
use tokio::time::Instant;
use tokio::time::Duration;
use tauri::State;
use tauri::{Manager, Emitter};
use log::{info, warn, error, debug};
use installer::{InstallerType, InstallerAction, Game};
use dlc_manager::DlcInfoWithState;
use std::sync::Arc;
use installer::{Game, InstallerAction, InstallerType};
use log::{debug, error, info, warn};
use parking_lot::Mutex;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::sync::Arc;
use tauri::State;
use tauri::{Emitter, Manager};
use tokio::time::Duration;
use tokio::time::Instant;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GameAction {
game_id: String,
action: String,
game_id: String,
action: String,
}
#[derive(Debug, Clone)]
struct DlcCache {
data: Vec<DlcInfoWithState>,
timestamp: Instant,
data: Vec<DlcInfoWithState>,
timestamp: Instant,
}
// Structure to hold the state of installed games
struct AppState {
games: Mutex<HashMap<String, Game>>,
dlc_cache: Mutex<HashMap<String, DlcCache>>,
fetch_cancellation: Arc<AtomicBool>,
games: Mutex<HashMap<String, Game>>,
dlc_cache: Mutex<HashMap<String, DlcCache>>,
fetch_cancellation: Arc<AtomicBool>,
}
#[tauri::command]
fn get_all_dlcs_command(game_path: String) -> Result<Vec<DlcInfoWithState>, String> {
info!("Getting all DLCs (enabled and disabled) for: {}", game_path);
dlc_manager::get_all_dlcs(&game_path)
info!("Getting all DLCs (enabled and disabled) for: {}", game_path);
dlc_manager::get_all_dlcs(&game_path)
}
// Scan and get the list of Steam games
#[tauri::command]
async fn scan_steam_games(state: State<'_, AppState>, app_handle: tauri::AppHandle) -> Result<Vec<Game>, String> {
async fn scan_steam_games(
state: State<'_, AppState>,
app_handle: tauri::AppHandle,
) -> Result<Vec<Game>, String> {
info!("Starting Steam games scan");
emit_scan_progress(&app_handle, "Locating Steam libraries...", 10);
// Get default Steam paths
let paths = searcher::get_default_steam_paths();
// Find Steam libraries
emit_scan_progress(&app_handle, "Finding Steam libraries...", 15);
let libraries = searcher::find_steam_libraries(&paths);
// Group libraries by path to avoid duplicates in logs
let mut unique_libraries = std::collections::HashSet::new();
for lib in &libraries {
unique_libraries.insert(lib.to_string_lossy().to_string());
}
info!("Found {} Steam library directories:", unique_libraries.len());
info!(
"Found {} Steam library directories:",
unique_libraries.len()
);
for (i, lib) in unique_libraries.iter().enumerate() {
info!(" Library {}: {}", i+1, lib);
info!(" Library {}: {}", i + 1, lib);
}
emit_scan_progress(&app_handle, &format!("Found {} Steam libraries. Starting game scan...", unique_libraries.len()), 20);
emit_scan_progress(
&app_handle,
&format!(
"Found {} Steam libraries. Starting game scan...",
unique_libraries.len()
),
20,
);
// Find installed games
let games_info = searcher::find_installed_games(&libraries).await;
emit_scan_progress(&app_handle, &format!("Found {} games. Processing...", games_info.len()), 90);
emit_scan_progress(
&app_handle,
&format!("Found {} games. Processing...", games_info.len()),
90,
);
// Log summary of games found
info!("Games scan complete - Found {} games", games_info.len());
info!("Native games: {}", games_info.iter().filter(|g| g.native).count());
info!("Proton games: {}", games_info.iter().filter(|g| !g.native).count());
info!("Games with CreamLinux: {}", games_info.iter().filter(|g| g.cream_installed).count());
info!("Games with SmokeAPI: {}", games_info.iter().filter(|g| g.smoke_installed).count());
info!(
"Native games: {}",
games_info.iter().filter(|g| g.native).count()
);
info!(
"Proton games: {}",
games_info.iter().filter(|g| !g.native).count()
);
info!(
"Games with CreamLinux: {}",
games_info.iter().filter(|g| g.cream_installed).count()
);
info!(
"Games with SmokeAPI: {}",
games_info.iter().filter(|g| g.smoke_installed).count()
);
// Convert to our Game struct
let mut result = Vec::new();
info!("Processing games into application state...");
for game_info in games_info {
// Only log detailed game info at Debug level to keep Info logs cleaner
debug!("Processing game: {}, Native: {}, CreamLinux: {}, SmokeAPI: {}",
game_info.title, game_info.native, game_info.cream_installed, game_info.smoke_installed);
debug!(
"Processing game: {}, Native: {}, CreamLinux: {}, SmokeAPI: {}",
game_info.title, game_info.native, game_info.cream_installed, game_info.smoke_installed
);
let game = Game {
id: game_info.id,
title: game_info.title,
@@ -105,383 +135,413 @@ async fn scan_steam_games(state: State<'_, AppState>, app_handle: tauri::AppHand
smoke_installed: game_info.smoke_installed,
installing: false,
};
result.push(game.clone());
// Store in state for later use
state.games.lock().insert(game.id.clone(), game);
}
emit_scan_progress(&app_handle, &format!("Scan complete. Found {} games.", result.len()), 100);
emit_scan_progress(
&app_handle,
&format!("Scan complete. Found {} games.", result.len()),
100,
);
info!("Game scan completed successfully");
Ok(result)
}
// Helper function to emit scan progress events
fn emit_scan_progress(app_handle: &tauri::AppHandle, message: &str, progress: u32) {
// Log first, then emit the event
info!("Scan progress: {}% - {}", progress, message);
let payload = serde_json::json!({
"message": message,
"progress": progress
});
if let Err(e) = app_handle.emit("scan-progress", payload) {
warn!("Failed to emit scan-progress event: {}", e);
}
// Log first, then emit the event
info!("Scan progress: {}% - {}", progress, message);
let payload = serde_json::json!({
"message": message,
"progress": progress
});
if let Err(e) = app_handle.emit("scan-progress", payload) {
warn!("Failed to emit scan-progress event: {}", e);
}
}
// Fetch game info by ID - useful for single game updates
#[tauri::command]
fn get_game_info(game_id: String, state: State<AppState>) -> Result<Game, String> {
let games = state.games.lock();
games.get(&game_id)
.cloned()
.ok_or_else(|| format!("Game with ID {} not found", game_id))
let games = state.games.lock();
games
.get(&game_id)
.cloned()
.ok_or_else(|| format!("Game with ID {} not found", game_id))
}
// Unified action handler for installation and uninstallation
#[tauri::command]
async fn process_game_action(
game_action: GameAction,
state: State<'_, AppState>,
app_handle: tauri::AppHandle
game_action: GameAction,
state: State<'_, AppState>,
app_handle: tauri::AppHandle,
) -> Result<Game, String> {
// Clone the information we need from state to avoid lifetime issues
let game = {
let games = state.games.lock();
games.get(&game_action.game_id)
.cloned()
.ok_or_else(|| format!("Game with ID {} not found", game_action.game_id))?
};
// Clone the information we need from state to avoid lifetime issues
let game = {
let games = state.games.lock();
games
.get(&game_action.game_id)
.cloned()
.ok_or_else(|| format!("Game with ID {} not found", game_action.game_id))?
};
// Parse the action string to determine type and operation
let (installer_type, action) = match game_action.action.as_str() {
"install_cream" => (InstallerType::Cream, InstallerAction::Install),
"uninstall_cream" => (InstallerType::Cream, InstallerAction::Uninstall),
"install_smoke" => (InstallerType::Smoke, InstallerAction::Install),
"uninstall_smoke" => (InstallerType::Smoke, InstallerAction::Uninstall),
_ => return Err(format!("Invalid action: {}", game_action.action))
};
// Parse the action string to determine type and operation
let (installer_type, action) = match game_action.action.as_str() {
"install_cream" => (InstallerType::Cream, InstallerAction::Install),
"uninstall_cream" => (InstallerType::Cream, InstallerAction::Uninstall),
"install_smoke" => (InstallerType::Smoke, InstallerAction::Install),
"uninstall_smoke" => (InstallerType::Smoke, InstallerAction::Uninstall),
_ => return Err(format!("Invalid action: {}", game_action.action)),
};
// Execute the action
installer::process_action(
game_action.game_id.clone(),
installer_type,
action,
game.clone(),
app_handle.clone()
).await?;
// Execute the action
installer::process_action(
game_action.game_id.clone(),
installer_type,
action,
game.clone(),
app_handle.clone(),
)
.await?;
// Update game status in state based on the action
let updated_game = {
let mut games_map = state.games.lock();
let game = games_map.get_mut(&game_action.game_id)
.ok_or_else(|| format!("Game with ID {} not found after action", game_action.game_id))?;
// Update installation status
match (installer_type, action) {
(InstallerType::Cream, InstallerAction::Install) => {
game.cream_installed = true;
},
(InstallerType::Cream, InstallerAction::Uninstall) => {
game.cream_installed = false;
},
(InstallerType::Smoke, InstallerAction::Install) => {
game.smoke_installed = true;
},
(InstallerType::Smoke, InstallerAction::Uninstall) => {
game.smoke_installed = false;
// Update game status in state based on the action
let updated_game = {
let mut games_map = state.games.lock();
let game = games_map.get_mut(&game_action.game_id).ok_or_else(|| {
format!(
"Game with ID {} not found after action",
game_action.game_id
)
})?;
// Update installation status
match (installer_type, action) {
(InstallerType::Cream, InstallerAction::Install) => {
game.cream_installed = true;
}
(InstallerType::Cream, InstallerAction::Uninstall) => {
game.cream_installed = false;
}
(InstallerType::Smoke, InstallerAction::Install) => {
game.smoke_installed = true;
}
(InstallerType::Smoke, InstallerAction::Uninstall) => {
game.smoke_installed = false;
}
}
// Reset installing flag
game.installing = false;
// Return updated game info
game.clone()
};
// Emit an event to update the UI for this specific game
if let Err(e) = app_handle.emit("game-updated", &updated_game) {
warn!("Failed to emit game-updated event: {}", e);
}
// Reset installing flag
game.installing = false;
// Return updated game info
game.clone()
};
// Removed cache update
// Emit an event to update the UI for this specific game
if let Err(e) = app_handle.emit("game-updated", &updated_game) {
warn!("Failed to emit game-updated event: {}", e);
}
Ok(updated_game)
Ok(updated_game)
}
// Fetch DLC list for a game
#[tauri::command]
async fn fetch_game_dlcs(game_id: String, app_handle: tauri::AppHandle) -> Result<Vec<DlcInfoWithState>, String> {
info!("Fetching DLCs for game ID: {}", game_id);
// Removed cache checking
// Always fetch fresh DLC data instead of using cache
match installer::fetch_dlc_details(&game_id).await {
Ok(dlcs) => {
// Convert to DlcInfoWithState (all enabled by default)
let dlcs_with_state = dlcs.into_iter()
.map(|dlc| DlcInfoWithState {
appid: dlc.appid,
name: dlc.name,
enabled: true,
})
.collect::<Vec<_>>();
// Cache in memory for this session (but not on disk)
let state = app_handle.state::<AppState>();
let mut cache = state.dlc_cache.lock();
cache.insert(game_id.clone(), DlcCache {
data: dlcs_with_state.clone(),
timestamp: Instant::now(),
});
Ok(dlcs_with_state)
},
Err(e) => Err(format!("Failed to fetch DLC details: {}", e))
}
async fn fetch_game_dlcs(
game_id: String,
app_handle: tauri::AppHandle,
) -> Result<Vec<DlcInfoWithState>, String> {
info!("Fetching DLCs for game ID: {}", game_id);
// Fetch DLC data
match installer::fetch_dlc_details(&game_id).await {
Ok(dlcs) => {
// Convert to DlcInfoWithState
let dlcs_with_state = dlcs
.into_iter()
.map(|dlc| DlcInfoWithState {
appid: dlc.appid,
name: dlc.name,
enabled: true,
})
.collect::<Vec<_>>();
// Cache in memory for this session (but not on disk)
let state = app_handle.state::<AppState>();
let mut cache = state.dlc_cache.lock();
cache.insert(
game_id.clone(),
DlcCache {
data: dlcs_with_state.clone(),
timestamp: Instant::now(),
},
);
Ok(dlcs_with_state)
}
Err(e) => Err(format!("Failed to fetch DLC details: {}", e)),
}
}
#[tauri::command]
fn abort_dlc_fetch(game_id: String, app_handle: tauri::AppHandle) -> Result<(), String> {
info!("Request to abort DLC fetch for game ID: {}", game_id);
let state = app_handle.state::<AppState>();
state.fetch_cancellation.store(true, Ordering::SeqCst);
// Reset after a short delay
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(500));
info!("Request to abort DLC fetch for game ID: {}", game_id);
let state = app_handle.state::<AppState>();
state.fetch_cancellation.store(false, Ordering::SeqCst);
});
Ok(())
state.fetch_cancellation.store(true, Ordering::SeqCst);
// Reset after a short delay
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(500));
let state = app_handle.state::<AppState>();
state.fetch_cancellation.store(false, Ordering::SeqCst);
});
Ok(())
}
// Fetch DLC list with progress updates (streaming)
#[tauri::command]
async fn stream_game_dlcs(game_id: String, app_handle: tauri::AppHandle) -> Result<(), String> {
info!("Streaming DLCs for game ID: {}", game_id);
// Removed cached DLC check - always fetch fresh data
// Always fetch fresh DLC data from API
match installer::fetch_dlc_details_with_progress(&game_id, &app_handle).await {
Ok(dlcs) => {
info!("Successfully streamed {} DLCs for game {}", dlcs.len(), game_id);
// Convert to DLCInfoWithState for in-memory caching only
let dlcs_with_state = dlcs.into_iter()
.map(|dlc| DlcInfoWithState {
appid: dlc.appid,
name: dlc.name,
enabled: true,
})
.collect::<Vec<_>>();
// Update in-memory cache without storing to disk
let state = app_handle.state::<AppState>();
let mut dlc_cache = state.dlc_cache.lock();
dlc_cache.insert(game_id.clone(), DlcCache {
data: dlcs_with_state,
timestamp: tokio::time::Instant::now(),
});
Ok(())
},
Err(e) => {
error!("Failed to stream DLC details: {}", e);
// Emit error event
let error_payload = serde_json::json!({
"error": format!("Failed to fetch DLC details: {}", e)
});
if let Err(emit_err) = app_handle.emit("dlc-error", error_payload) {
warn!("Failed to emit dlc-error event: {}", emit_err);
}
Err(format!("Failed to fetch DLC details: {}", e))
}
}
info!("Streaming DLCs for game ID: {}", game_id);
// Fetch DLC data from API
match installer::fetch_dlc_details_with_progress(&game_id, &app_handle).await {
Ok(dlcs) => {
info!(
"Successfully streamed {} DLCs for game {}",
dlcs.len(),
game_id
);
// Convert to DLCInfoWithState for in-memory caching only
let dlcs_with_state = dlcs
.into_iter()
.map(|dlc| DlcInfoWithState {
appid: dlc.appid,
name: dlc.name,
enabled: true,
})
.collect::<Vec<_>>();
// Update in-memory cache without storing to disk
let state = app_handle.state::<AppState>();
let mut dlc_cache = state.dlc_cache.lock();
dlc_cache.insert(
game_id.clone(),
DlcCache {
data: dlcs_with_state,
timestamp: tokio::time::Instant::now(),
},
);
Ok(())
}
Err(e) => {
error!("Failed to stream DLC details: {}", e);
// Emit error event
let error_payload = serde_json::json!({
"error": format!("Failed to fetch DLC details: {}", e)
});
if let Err(emit_err) = app_handle.emit("dlc-error", error_payload) {
warn!("Failed to emit dlc-error event: {}", emit_err);
}
Err(format!("Failed to fetch DLC details: {}", e))
}
}
}
// Clear caches command renamed to flush_data for clarity
#[tauri::command]
fn clear_caches() -> Result<(), String> {
info!("Data flush requested - cleaning in-memory state only");
Ok(())
info!("Data flush requested - cleaning in-memory state only");
Ok(())
}
// Get the list of enabled DLCs for a game
#[tauri::command]
fn get_enabled_dlcs_command(game_path: String) -> Result<Vec<String>, String> {
info!("Getting enabled DLCs for: {}", game_path);
dlc_manager::get_enabled_dlcs(&game_path)
info!("Getting enabled DLCs for: {}", game_path);
dlc_manager::get_enabled_dlcs(&game_path)
}
// Update the DLC configuration for a game
#[tauri::command]
fn update_dlc_configuration_command(game_path: String, dlcs: Vec<DlcInfoWithState>) -> Result<(), String> {
info!("Updating DLC configuration for: {}", game_path);
dlc_manager::update_dlc_configuration(&game_path, dlcs)
fn update_dlc_configuration_command(
game_path: String,
dlcs: Vec<DlcInfoWithState>,
) -> Result<(), String> {
info!("Updating DLC configuration for: {}", game_path);
dlc_manager::update_dlc_configuration(&game_path, dlcs)
}
// Install CreamLinux with selected DLCs
#[tauri::command]
async fn install_cream_with_dlcs_command(
game_id: String,
selected_dlcs: Vec<DlcInfoWithState>,
app_handle: tauri::AppHandle
game_id: String,
selected_dlcs: Vec<DlcInfoWithState>,
app_handle: tauri::AppHandle,
) -> Result<Game, String> {
info!("Installing CreamLinux with selected DLCs for game: {}", game_id);
// Clone selected_dlcs for later use
let selected_dlcs_clone = selected_dlcs.clone();
// Install CreamLinux with the selected DLCs
match dlc_manager::install_cream_with_dlcs(game_id.clone(), app_handle.clone(), selected_dlcs).await {
Ok(_) => {
// Return updated game info
let state = app_handle.state::<AppState>();
// Get a mutable reference and update the game
let game = {
let mut games_map = state.games.lock();
let game = games_map.get_mut(&game_id)
.ok_or_else(|| format!("Game with ID {} not found after installation", game_id))?;
// Update installation status
game.cream_installed = true;
game.installing = false;
// Clone the game for returning later
game.clone()
}; // mutable borrow ends here
// Removed game caching
// Emit an event to update the UI
if let Err(e) = app_handle.emit("game-updated", &game) {
warn!("Failed to emit game-updated event: {}", e);
}
// Show installation complete dialog with instructions
let instructions = installer::InstallationInstructions {
type_: "cream_install".to_string(),
command: "sh ./cream.sh %command%".to_string(),
game_title: game.title.clone(),
dlc_count: Some(selected_dlcs_clone.iter().filter(|dlc| dlc.enabled).count())
};
installer::emit_progress(
&app_handle,
&format!("Installation Completed: {}", game.title),
"CreamLinux has been installed successfully!",
100.0,
true,
true,
Some(instructions)
);
Ok(game)
},
Err(e) => {
error!("Failed to install CreamLinux with selected DLCs: {}", e);
Err(format!("Failed to install CreamLinux with selected DLCs: {}", e))
}
}
info!(
"Installing CreamLinux with selected DLCs for game: {}",
game_id
);
// Clone selected_dlcs for later use
let selected_dlcs_clone = selected_dlcs.clone();
// Install CreamLinux with the selected DLCs
match dlc_manager::install_cream_with_dlcs(game_id.clone(), app_handle.clone(), selected_dlcs)
.await
{
Ok(_) => {
// Return updated game info
let state = app_handle.state::<AppState>();
// Get a mutable reference and update the game
let game = {
let mut games_map = state.games.lock();
let game = games_map.get_mut(&game_id).ok_or_else(|| {
format!("Game with ID {} not found after installation", game_id)
})?;
// Update installation status
game.cream_installed = true;
game.installing = false;
// Clone the game for returning later
game.clone()
};
// Emit an event to update the UI
if let Err(e) = app_handle.emit("game-updated", &game) {
warn!("Failed to emit game-updated event: {}", e);
}
// Show installation complete dialog with instructions
let instructions = installer::InstallationInstructions {
type_: "cream_install".to_string(),
command: "sh ./cream.sh %command%".to_string(),
game_title: game.title.clone(),
dlc_count: Some(selected_dlcs_clone.iter().filter(|dlc| dlc.enabled).count()),
};
installer::emit_progress(
&app_handle,
&format!("Installation Completed: {}", game.title),
"CreamLinux has been installed successfully!",
100.0,
true,
true,
Some(instructions),
);
Ok(game)
}
Err(e) => {
error!("Failed to install CreamLinux with selected DLCs: {}", e);
Err(format!(
"Failed to install CreamLinux with selected DLCs: {}",
e
))
}
}
}
// Setup logging
fn setup_logging() -> Result<(), Box<dyn std::error::Error>> {
use log::LevelFilter;
use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Config, Root};
use log4rs::encode::pattern::PatternEncoder;
use std::fs;
// Get XDG cache directory
let xdg_dirs = xdg::BaseDirectories::with_prefix("creamlinux")?;
let log_path = xdg_dirs.place_cache_file("creamlinux.log")?;
// Clear the log file on startup
if log_path.exists() {
if let Err(e) = fs::write(&log_path, "") {
eprintln!("Warning: Failed to clear log file: {}", e);
}
}
// Create a file appender with improved log format
let file = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new(
"[{d(%Y-%m-%d %H:%M:%S)}] {l}: {m}\n"
)))
.build(log_path)?;
// Build the config
let config = Config::builder()
.appender(Appender::builder().build("file", Box::new(file)))
.build(Root::builder().appender("file").build(LevelFilter::Info))?;
// Initialize log4rs with this config
log4rs::init_config(config)?;
info!("CreamLinux started with a clean log file");
Ok(())
use log::LevelFilter;
use log4rs::append::file::FileAppender;
use log4rs::config::{Appender, Config, Root};
use log4rs::encode::pattern::PatternEncoder;
use std::fs;
// Get XDG cache directory
let xdg_dirs = xdg::BaseDirectories::with_prefix("creamlinux")?;
let log_path = xdg_dirs.place_cache_file("creamlinux.log")?;
// Clear the log file on startup
if log_path.exists() {
if let Err(e) = fs::write(&log_path, "") {
eprintln!("Warning: Failed to clear log file: {}", e);
}
}
// Create a file appender
let file = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new(
"[{d(%Y-%m-%d %H:%M:%S)}] {l}: {m}\n",
)))
.build(log_path)?;
// Build the config
let config = Config::builder()
.appender(Appender::builder().build("file", Box::new(file)))
.build(Root::builder().appender("file").build(LevelFilter::Info))?;
// Initialize log4rs with this config
log4rs::init_config(config)?;
info!("CreamLinux started with a clean log file");
Ok(())
}
fn main() {
// Set up logging first
if let Err(e) = setup_logging() {
eprintln!("Warning: Failed to initialize logging: {}", e);
}
info!("Initializing CreamLinux application");
let app_state = AppState {
games: Mutex::new(HashMap::new()),
dlc_cache: Mutex::new(HashMap::new()),
fetch_cancellation: Arc::new(AtomicBool::new(false)),
};
// Set up logging first
if let Err(e) = setup_logging() {
eprintln!("Warning: Failed to initialize logging: {}", e);
}
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.manage(app_state)
.invoke_handler(tauri::generate_handler![
scan_steam_games,
get_game_info,
process_game_action,
fetch_game_dlcs,
stream_game_dlcs,
get_enabled_dlcs_command,
update_dlc_configuration_command,
install_cream_with_dlcs_command,
get_all_dlcs_command,
clear_caches,
abort_dlc_fetch,
])
.setup(|app| {
// Add a setup handler to do any initialization work
info!("Tauri application setup");
#[cfg(debug_assertions)]
{
if std::env::var("OPEN_DEVTOOLS").ok().as_deref() == Some("1") {
if let Some(window) = app.get_webview_window("main") {
window.open_devtools();
}
}
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
info!("Initializing CreamLinux application");
let app_state = AppState {
games: Mutex::new(HashMap::new()),
dlc_cache: Mutex::new(HashMap::new()),
fetch_cancellation: Arc::new(AtomicBool::new(false)),
};
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.manage(app_state)
.invoke_handler(tauri::generate_handler![
scan_steam_games,
get_game_info,
process_game_action,
fetch_game_dlcs,
stream_game_dlcs,
get_enabled_dlcs_command,
update_dlc_configuration_command,
install_cream_with_dlcs_command,
get_all_dlcs_command,
clear_caches,
abort_dlc_fetch,
])
.setup(|app| {
// Add a setup handler to do any initialization work
info!("Tauri application setup");
#[cfg(debug_assertions)]
{
if std::env::var("OPEN_DEVTOOLS").ok().as_deref() == Some("1") {
if let Some(window) = app.get_webview_window("main") {
window.open_devtools();
}
}
}
Ok(())
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -1,15 +1,14 @@
// src/searcher.rs
use log::{debug, error, info, warn};
use regex::Regex;
use std::collections::HashSet;
use std::fs;
use std::io::Read;
use std::path::{Path, PathBuf};
use std::collections::HashSet;
use log::{info, debug, warn, error};
use regex::Regex;
use walkdir::WalkDir;
use tokio::sync::mpsc;
use std::sync::Arc;
use tokio::sync::mpsc;
use walkdir::WalkDir;
/// Game information structure
// Game information structure
#[derive(Debug, Clone)]
pub struct GameInfo {
pub id: String,
@@ -21,24 +20,24 @@ pub struct GameInfo {
pub smoke_installed: bool,
}
/// Find potential Steam installation directories
// Find potential Steam installation directories
pub fn get_default_steam_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
// Get user's home directory
if let Ok(home) = std::env::var("HOME") {
info!("Searching for Steam in home directory: {}", home);
// Common Steam installation locations on Linux
let common_paths = [
".steam/steam", // Steam symlink directory
".steam/root", // Alternative symlink
".local/share/Steam", // Flatpak Steam installation
".var/app/com.valvesoftware.Steam/.local/share/Steam", // Flatpak container path
".var/app/com.valvesoftware.Steam/data/Steam", // Alternative Flatpak path
"/run/media/mmcblk0p1", // Removable Storage path
".steam/steam", // Steam symlink directory
".steam/root", // Alternative symlink
".local/share/Steam", // Flatpak Steam installation
".var/app/com.valvesoftware.Steam/.local/share/Steam", // Flatpak container path
".var/app/com.valvesoftware.Steam/data/Steam", // Alternative Flatpak path
"/run/media/mmcblk0p1", // Removable Storage path
];
for path in &common_paths {
let full_path = PathBuf::from(&home).join(path);
if full_path.exists() {
@@ -47,13 +46,10 @@ pub fn get_default_steam_paths() -> Vec<PathBuf> {
}
}
}
// Add Steam Deck paths if they exist (these don't rely on HOME)
let deck_paths = [
"/home/deck/.steam/steam",
"/home/deck/.local/share/Steam",
];
// Add Steam Deck paths if they exist
let deck_paths = ["/home/deck/.steam/steam", "/home/deck/.local/share/Steam"];
for path in &deck_paths {
let p = PathBuf::from(path);
if p.exists() && !paths.contains(&p) {
@@ -61,7 +57,7 @@ pub fn get_default_steam_paths() -> Vec<PathBuf> {
paths.push(p);
}
}
// Try to extract paths from Steam registry file
if let Some(registry_paths) = read_steam_registry() {
for path in registry_paths {
@@ -71,39 +67,39 @@ pub fn get_default_steam_paths() -> Vec<PathBuf> {
}
}
}
info!("Found {} potential Steam directories", paths.len());
paths
}
/// Try to read the Steam registry file to find installation paths
// Try to read the Steam registry file to find installation paths
fn read_steam_registry() -> Option<Vec<PathBuf>> {
let home = match std::env::var("HOME") {
Ok(h) => h,
Err(_) => return None,
};
let registry_paths = [
format!("{}/.steam/registry.vdf", home),
format!("{}/.steam/steam/registry.vdf", home),
format!("{}/.local/share/Steam/registry.vdf", home),
];
for registry_path in registry_paths {
let path = Path::new(&registry_path);
if path.exists() {
debug!("Found Steam registry at: {}", path.display());
if let Ok(content) = fs::read_to_string(path) {
let mut paths = Vec::new();
// Extract Steam installation paths
let re_steam_path = Regex::new(r#""SteamPath"\s+"([^"]+)""#).unwrap();
if let Some(cap) = re_steam_path.captures(&content) {
let steam_path = PathBuf::from(&cap[1]);
paths.push(steam_path);
}
// Look for install path
let re_install_path = Regex::new(r#""InstallPath"\s+"([^"]+)""#).unwrap();
if let Some(cap) = re_install_path.captures(&content) {
@@ -112,84 +108,84 @@ fn read_steam_registry() -> Option<Vec<PathBuf>> {
paths.push(install_path);
}
}
if !paths.is_empty() {
return Some(paths);
}
}
}
}
None
}
/// Find all Steam library folders from base Steam installation paths
// Find all Steam library folders from base Steam installation paths
pub fn find_steam_libraries(base_paths: &[PathBuf]) -> Vec<PathBuf> {
let mut libraries = HashSet::new();
for base_path in base_paths {
debug!("Looking for Steam libraries in: {}", base_path.display());
// Check if this path contains a steamapps directory
let steamapps_path = base_path.join("steamapps");
if steamapps_path.exists() && steamapps_path.is_dir() {
debug!("Found steamapps directory: {}", steamapps_path.display());
libraries.insert(steamapps_path.clone());
// Check for additional libraries in libraryfolders.vdf
parse_library_folders_vdf(&steamapps_path, &mut libraries);
}
// Also check for steamapps in common locations relative to this path
let possible_steamapps = [
base_path.join("steam/steamapps"),
base_path.join("Steam/steamapps"),
];
for path in &possible_steamapps {
if path.exists() && path.is_dir() && !libraries.contains(path) {
debug!("Found steamapps directory: {}", path.display());
libraries.insert(path.clone());
// Check for additional libraries in libraryfolders.vdf
parse_library_folders_vdf(path, &mut libraries);
}
}
}
let result: Vec<PathBuf> = libraries.into_iter().collect();
info!("Found {} Steam library directories", result.len());
for (i, lib) in result.iter().enumerate() {
info!(" Library {}: {}", i+1, lib.display());
info!(" Library {}: {}", i + 1, lib.display());
}
result
}
/// Parse libraryfolders.vdf to extract additional library paths
// Parse libraryfolders.vdf to extract additional library paths
fn parse_library_folders_vdf(steamapps_path: &Path, libraries: &mut HashSet<PathBuf>) {
// Check both possible locations of the VDF file
let vdf_paths = [
steamapps_path.join("libraryfolders.vdf"),
steamapps_path.join("config/libraryfolders.vdf"),
];
for vdf_path in &vdf_paths {
if vdf_path.exists() {
debug!("Found library folders VDF: {}", vdf_path.display());
if let Ok(content) = fs::read_to_string(vdf_path) {
// Extract library paths using regex for both new and old format VDFs
let re_path = Regex::new(r#""path"\s+"([^"]+)""#).unwrap();
for cap in re_path.captures_iter(&content) {
let path_str = &cap[1];
let lib_path = PathBuf::from(path_str).join("steamapps");
if lib_path.exists() && lib_path.is_dir() && !libraries.contains(&lib_path) {
debug!("Found library from VDF: {}", lib_path.display());
// Clone lib_path before inserting to avoid ownership issues
let lib_path_clone = lib_path.clone();
libraries.insert(lib_path_clone);
// Recursively check this library for more libraries
parse_library_folders_vdf(&lib_path, libraries);
}
@@ -199,7 +195,7 @@ fn parse_library_folders_vdf(steamapps_path: &Path, libraries: &mut HashSet<Path
}
}
/// Parse an appmanifest ACF file to extract game information
// Parse an appmanifest ACF file to extract game information
fn parse_appmanifest(path: &Path) -> Option<(String, String, String)> {
match fs::read_to_string(path) {
Ok(content) => {
@@ -207,16 +203,16 @@ fn parse_appmanifest(path: &Path) -> Option<(String, String, String)> {
let re_appid = Regex::new(r#""appid"\s+"(\d+)""#).unwrap();
let re_name = Regex::new(r#""name"\s+"([^"]+)""#).unwrap();
let re_installdir = Regex::new(r#""installdir"\s+"([^"]+)""#).unwrap();
if let (Some(app_id_cap), Some(name_cap), Some(dir_cap)) = (
re_appid.captures(&content),
re_name.captures(&content),
re_installdir.captures(&content)
re_installdir.captures(&content),
) {
let app_id = app_id_cap[1].to_string();
let name = name_cap[1].to_string();
let install_dir = dir_cap[1].to_string();
return Some((app_id, name, install_dir));
}
}
@@ -224,364 +220,387 @@ fn parse_appmanifest(path: &Path) -> Option<(String, String, String)> {
error!("Failed to read ACF file {}: {}", path.display(), e);
}
}
None
}
/// Check if a file is a Linux ELF binary
// Check if a file is a Linux ELF binary
fn is_elf_binary(path: &Path) -> bool {
if let Ok(mut file) = fs::File::open(path) {
let mut buffer = [0; 4];
if file.read_exact(&mut buffer).is_ok() {
// Check for ELF magic number (0x7F 'E' 'L' 'F')
return buffer[0] == 0x7F && buffer[1] == b'E' && buffer[2] == b'L' && buffer[3] == b'F';
return buffer[0] == 0x7F
&& buffer[1] == b'E'
&& buffer[2] == b'L'
&& buffer[3] == b'F';
}
}
false
}
/// Check if a game has CreamLinux installed
// Check if a game has CreamLinux installed
fn check_creamlinux_installed(game_path: &Path) -> bool {
let cream_files = [
"cream.sh",
"cream_api.ini",
"cream_api.so",
];
let cream_files = ["cream.sh", "cream_api.ini", "cream_api.so"];
for file in &cream_files {
if game_path.join(file).exists() {
debug!("CreamLinux installation detected: {}", file);
return true;
}
}
false
}
/// Check if a game has SmokeAPI installed
// Check if a game has SmokeAPI installed
fn check_smokeapi_installed(game_path: &Path, api_files: &[String]) -> bool {
if api_files.is_empty() {
return false;
}
// SmokeAPI creates backups with _o.dll suffix
for api_file in api_files {
let api_path = game_path.join(api_file);
let api_dir = api_path.parent().unwrap_or(game_path);
let api_filename = api_path.file_name().unwrap_or_default();
// Check for backup file (original file renamed with _o.dll suffix)
let backup_name = api_filename.to_string_lossy().replace(".dll", "_o.dll");
let backup_path = api_dir.join(backup_name);
if backup_path.exists() {
debug!("SmokeAPI backup file found: {}", backup_path.display());
return true;
}
}
false
}
/// Scan a game directory to determine if it's native or needs Proton
/// Also collect any Steam API DLLs for potential SmokeAPI installation
// Scan a game directory to determine if it's native or needs Proton
// Also collect any Steam API DLLs for potential SmokeAPI installation
fn scan_game_directory(game_path: &Path) -> (bool, Vec<String>) {
let mut found_exe = false;
let mut found_linux_binary = false;
let mut steam_api_files = Vec::new();
// Directories to skip for better performance
let skip_dirs = [
"videos", "video", "movies", "movie",
"sound", "sounds", "audio",
"textures", "music", "localization",
"shaders", "logs", "assets/audio",
"assets/video", "assets/textures"
];
// Only scan to a reasonable depth (avoid extreme recursion)
const MAX_DEPTH: usize = 8;
// File extensions to check for (executable and Steam API files)
let exe_extensions = ["exe", "bat", "cmd", "msi"];
let binary_extensions = ["so", "bin", "sh", "x86", "x86_64"];
// Recursively walk through the game directory with optimized settings
for entry in WalkDir::new(game_path)
.max_depth(MAX_DEPTH) // Limit depth to avoid traversing too deep
.follow_links(false) // Don't follow symlinks to prevent cycles
.into_iter()
.filter_entry(|e| {
// Skip certain directories for performance
if e.file_type().is_dir() {
let file_name = e.file_name().to_string_lossy().to_lowercase();
if skip_dirs.iter().any(|&dir| file_name == dir) {
debug!("Skipping directory: {}", e.path().display());
return false;
}
}
true
})
.filter_map(Result::ok) {
let path = entry.path();
if !path.is_file() {
continue;
}
// Check file extension
if let Some(ext) = path.extension() {
let ext_str = ext.to_string_lossy().to_lowercase();
// Check for Windows executables
if exe_extensions.iter().any(|&e| ext_str == e) {
found_exe = true;
}
// Check for Steam API DLLs
if ext_str == "dll" {
let filename = path.file_name().unwrap_or_default().to_string_lossy().to_lowercase();
if filename == "steam_api.dll" || filename == "steam_api64.dll" {
if let Ok(rel_path) = path.strip_prefix(game_path) {
let rel_path_str = rel_path.to_string_lossy().to_string();
debug!("Found Steam API DLL: {}", rel_path_str);
steam_api_files.push(rel_path_str);
}
}
}
// Check for Linux binary files
if binary_extensions.iter().any(|&e| ext_str == e) {
found_linux_binary = true;
// Check if it's actually an ELF binary for more certainty
if ext_str == "so" && is_elf_binary(path) {
found_linux_binary = true;
}
}
}
// Check for Linux executables (no extension)
#[cfg(unix)]
if !path.extension().is_some() {
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = path.metadata() {
let is_executable = metadata.permissions().mode() & 0o111 != 0;
// Check executable permission and ELF format
if is_executable && is_elf_binary(path) {
found_linux_binary = true;
}
}
}
// If we've found enough evidence for both platforms and Steam API DLLs, we can stop
// This early break greatly improves performance for large game directories
if found_exe && found_linux_binary && !steam_api_files.is_empty() {
debug!("Found sufficient evidence, breaking scan early");
break;
}
}
// A game is considered native if it has Linux binaries but no Windows executables
let is_native = found_linux_binary && !found_exe;
debug!("Game scan results: native={}, exe={}, api_dlls={}", is_native, found_exe, steam_api_files.len());
(is_native, steam_api_files)
let mut found_exe = false;
let mut found_linux_binary = false;
let mut steam_api_files = Vec::new();
// Directories to skip for better performance
let skip_dirs = [
"videos",
"video",
"movies",
"movie",
"sound",
"sounds",
"audio",
"textures",
"music",
"localization",
"shaders",
"logs",
"assets/audio",
"assets/video",
"assets/textures",
];
// Only scan to a reasonable depth (avoid extreme recursion)
const MAX_DEPTH: usize = 8;
// File extensions to check for (executable and Steam API files)
let exe_extensions = ["exe", "bat", "cmd", "msi"];
let binary_extensions = ["so", "bin", "sh", "x86", "x86_64"];
// Recursively walk through the game directory
for entry in WalkDir::new(game_path)
.max_depth(MAX_DEPTH) // Limit depth to avoid traversing too deep
.follow_links(false) // Don't follow symlinks to prevent cycles
.into_iter()
.filter_entry(|e| {
// Skip certain directories for performance
if e.file_type().is_dir() {
let file_name = e.file_name().to_string_lossy().to_lowercase();
if skip_dirs.iter().any(|&dir| file_name == dir) {
debug!("Skipping directory: {}", e.path().display());
return false;
}
}
true
})
.filter_map(Result::ok)
{
let path = entry.path();
if !path.is_file() {
continue;
}
// Check file extension
if let Some(ext) = path.extension() {
let ext_str = ext.to_string_lossy().to_lowercase();
// Check for Windows executables
if exe_extensions.iter().any(|&e| ext_str == e) {
found_exe = true;
}
// Check for Steam API DLLs
if ext_str == "dll" {
let filename = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_lowercase();
if filename == "steam_api.dll" || filename == "steam_api64.dll" {
if let Ok(rel_path) = path.strip_prefix(game_path) {
let rel_path_str = rel_path.to_string_lossy().to_string();
debug!("Found Steam API DLL: {}", rel_path_str);
steam_api_files.push(rel_path_str);
}
}
}
// Check for Linux binary files
if binary_extensions.iter().any(|&e| ext_str == e) {
found_linux_binary = true;
// Check if it's actually an ELF binary for more certainty
if ext_str == "so" && is_elf_binary(path) {
found_linux_binary = true;
}
}
}
// Check for Linux executables (no extension)
#[cfg(unix)]
if !path.extension().is_some() {
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = path.metadata() {
let is_executable = metadata.permissions().mode() & 0o111 != 0;
// Check executable permission and ELF format
if is_executable && is_elf_binary(path) {
found_linux_binary = true;
}
}
}
// If we've found enough evidence for both platforms and Steam API DLLs, we can stop
if found_exe && found_linux_binary && !steam_api_files.is_empty() {
debug!("Found sufficient evidence, breaking scan early");
break;
}
}
// A game is considered native if it has Linux binaries but no Windows executables
let is_native = found_linux_binary && !found_exe;
debug!(
"Game scan results: native={}, exe={}, api_dlls={}",
is_native,
found_exe,
steam_api_files.len()
);
(is_native, steam_api_files)
}
/// Find all installed Steam games from library folders
// Find all installed Steam games from library folders
pub async fn find_installed_games(steamapps_paths: &[PathBuf]) -> Vec<GameInfo> {
let mut games = Vec::new();
let seen_ids = Arc::new(tokio::sync::Mutex::new(HashSet::new()));
// IDs to skip (tools, redistributables, etc.)
let skip_ids = Arc::new([
"228980", // Steamworks Common Redistributables
"1070560", // Steam Linux Runtime
"1391110", // Steam Linux Runtime - Soldier
"1628350", // Steam Linux Runtime - Sniper
"1493710", // Proton Experimental
"2180100", // Steam Linux Runtime - Scout
].iter().copied().collect::<HashSet<&str>>());
// Name patterns to skip (case insensitive)
let skip_patterns = Arc::new(
[
r"(?i)steam linux runtime",
r"(?i)proton",
r"(?i)steamworks common",
r"(?i)redistributable",
r"(?i)dotnet",
r"(?i)vc redist",
]
.iter()
.map(|pat| Regex::new(pat).unwrap())
.collect::<Vec<_>>()
);
info!("Scanning for installed games in parallel...");
// Create a channel to collect results
let (tx, mut rx) = mpsc::channel(32);
// First collect all appmanifest files to process
let mut app_manifests = Vec::new();
for steamapps_dir in steamapps_paths {
if let Ok(entries) = fs::read_dir(steamapps_dir) {
for entry in entries.flatten() {
let path = entry.path();
let filename = path.file_name().unwrap_or_default().to_string_lossy();
// Check for appmanifest files
if filename.starts_with("appmanifest_") && filename.ends_with(".acf") {
app_manifests.push((path, steamapps_dir.clone()));
}
}
}
}
info!("Found {} appmanifest files to process", app_manifests.len());
// Process each appmanifest file in parallel with a maximum concurrency
let max_concurrent = num_cpus::get().max(1).min(8); // Use between 1 and 8 CPU cores
info!("Using {} concurrent scanners", max_concurrent);
// Use a semaphore to limit concurrency
let semaphore = Arc::new(tokio::sync::Semaphore::new(max_concurrent));
// Create a Vec to store all our task handles
let mut handles = Vec::new();
// Process each manifest file
for (manifest_idx, (path, steamapps_dir)) in app_manifests.iter().enumerate() {
// Clone what we need for the task
let path = path.clone();
let steamapps_dir = steamapps_dir.clone();
let skip_patterns = Arc::clone(&skip_patterns);
let tx = tx.clone();
let seen_ids = Arc::clone(&seen_ids);
let semaphore = Arc::clone(&semaphore);
let skip_ids = Arc::clone(&skip_ids);
// Create a new task
let handle = tokio::spawn(async move {
// Acquire a permit from the semaphore
let _permit = semaphore.acquire().await.unwrap();
// Parse the appmanifest file
if let Some((id, name, install_dir)) = parse_appmanifest(&path) {
// Skip if in exclusion list
if skip_ids.contains(id.as_str()) {
return;
}
// Add a guard against duplicates
{
let mut seen = seen_ids.lock().await;
if seen.contains(&id) {
return;
}
seen.insert(id.clone());
}
// Skip if the name matches any exclusion patterns
if skip_patterns.iter().any(|re| re.is_match(&name)) {
debug!("Skipping runtime/tool: {} ({})", name, id);
return;
}
// Full path to the game directory
let game_path = steamapps_dir.join("common").join(&install_dir);
// Skip if game directory doesn't exist
if !game_path.exists() {
warn!("Game directory not found: {}", game_path.display());
return;
}
// Scan the game directory to determine platform and find Steam API DLLs
info!("Scanning game: {} at {}", name, game_path.display());
// Scanning is I/O heavy but not CPU heavy, so we can just do it directly
let (is_native, api_files) = scan_game_directory(&game_path);
// Check for CreamLinux installation
let cream_installed = check_creamlinux_installed(&game_path);
// Check for SmokeAPI installation (only for non-native games with Steam API DLLs)
let smoke_installed = if !is_native && !api_files.is_empty() {
check_smokeapi_installed(&game_path, &api_files)
} else {
false
};
// Create the game info
let game_info = GameInfo {
id,
title: name,
path: game_path,
native: is_native,
api_files,
cream_installed,
smoke_installed,
};
// Send the game info through the channel
if tx.send(game_info).await.is_err() {
error!("Failed to send game info through channel");
}
}
});
handles.push(handle);
// Every 10 files, yield to allow progress updates
if manifest_idx % 10 == 0 {
// We would update progress here in a full implementation
tokio::task::yield_now().await;
}
}
// Drop the original sender so the receiver knows when we're done
drop(tx);
// Spawn a task to collect all the results
let receiver_task = tokio::spawn(async move {
let mut results = Vec::new();
while let Some(game) = rx.recv().await {
info!("Found game: {} ({})", game.title, game.id);
info!(" Path: {}", game.path.display());
info!(" Status: Native={}, Cream={}, Smoke={}",
game.native, game.cream_installed, game.smoke_installed);
// Log Steam API DLLs if any
if !game.api_files.is_empty() {
info!(" Steam API files:");
for api_file in &game.api_files {
info!(" - {}", api_file);
}
}
results.push(game);
}
results
});
// Wait for all scan tasks to complete - but don't wait for the results yet
for handle in handles {
// Ignore errors - the receiver task will just get fewer results
let _ = handle.await;
}
// Now wait for all results to be collected
if let Ok(results) = receiver_task.await {
games = results;
}
info!("Found {} installed games", games.len());
games
}
let mut games = Vec::new();
let seen_ids = Arc::new(tokio::sync::Mutex::new(HashSet::new()));
// IDs to skip (tools, redistributables, etc.)
let skip_ids = Arc::new(
[
"228980", // Steamworks Common Redistributables
"1070560", // Steam Linux Runtime
"1391110", // Steam Linux Runtime - Soldier
"1628350", // Steam Linux Runtime - Sniper
"1493710", // Proton Experimental
"2180100", // Steam Linux Runtime - Scout
]
.iter()
.copied()
.collect::<HashSet<&str>>(),
);
// Name patterns to skip (case insensitive)
let skip_patterns = Arc::new(
[
r"(?i)steam linux runtime",
r"(?i)proton",
r"(?i)steamworks common",
r"(?i)redistributable",
r"(?i)dotnet",
r"(?i)vc redist",
]
.iter()
.map(|pat| Regex::new(pat).unwrap())
.collect::<Vec<_>>(),
);
info!("Scanning for installed games in parallel...");
// Create a channel to collect results
let (tx, mut rx) = mpsc::channel(32);
// First collect all appmanifest files to process
let mut app_manifests = Vec::new();
for steamapps_dir in steamapps_paths {
if let Ok(entries) = fs::read_dir(steamapps_dir) {
for entry in entries.flatten() {
let path = entry.path();
let filename = path.file_name().unwrap_or_default().to_string_lossy();
// Check for appmanifest files
if filename.starts_with("appmanifest_") && filename.ends_with(".acf") {
app_manifests.push((path, steamapps_dir.clone()));
}
}
}
}
info!("Found {} appmanifest files to process", app_manifests.len());
// Process appmanifest files
let max_concurrent = num_cpus::get().max(1).min(8); // Use between 1 and 8 CPU cores
info!("Using {} concurrent scanners", max_concurrent);
// Use a semaphore to limit concurrency
let semaphore = Arc::new(tokio::sync::Semaphore::new(max_concurrent));
// Create a Vec to store all our task handles
let mut handles = Vec::new();
// Process each manifest file
for (manifest_idx, (path, steamapps_dir)) in app_manifests.iter().enumerate() {
// Clone what we need for the task
let path = path.clone();
let steamapps_dir = steamapps_dir.clone();
let skip_patterns = Arc::clone(&skip_patterns);
let tx = tx.clone();
let seen_ids = Arc::clone(&seen_ids);
let semaphore = Arc::clone(&semaphore);
let skip_ids = Arc::clone(&skip_ids);
// Create a new task
let handle = tokio::spawn(async move {
// Acquire a permit from the semaphore
let _permit = semaphore.acquire().await.unwrap();
// Parse the appmanifest file
if let Some((id, name, install_dir)) = parse_appmanifest(&path) {
// Skip if in exclusion list
if skip_ids.contains(id.as_str()) {
return;
}
// Add a guard against duplicates
{
let mut seen = seen_ids.lock().await;
if seen.contains(&id) {
return;
}
seen.insert(id.clone());
}
// Skip if the name matches any exclusion patterns
if skip_patterns.iter().any(|re| re.is_match(&name)) {
debug!("Skipping runtime/tool: {} ({})", name, id);
return;
}
// Full path to the game directory
let game_path = steamapps_dir.join("common").join(&install_dir);
// Skip if game directory doesn't exist
if !game_path.exists() {
warn!("Game directory not found: {}", game_path.display());
return;
}
// Scan the game directory to determine platform and find Steam API DLLs
info!("Scanning game: {} at {}", name, game_path.display());
// Scanning is I/O heavy but not CPU heavy, so we can just do it directly
let (is_native, api_files) = scan_game_directory(&game_path);
// Check for CreamLinux installation
let cream_installed = check_creamlinux_installed(&game_path);
// Check for SmokeAPI installation (only for non-native games with Steam API DLLs)
let smoke_installed = if !is_native && !api_files.is_empty() {
check_smokeapi_installed(&game_path, &api_files)
} else {
false
};
// Create the game info
let game_info = GameInfo {
id,
title: name,
path: game_path,
native: is_native,
api_files,
cream_installed,
smoke_installed,
};
// Send the game info through the channel
if tx.send(game_info).await.is_err() {
error!("Failed to send game info through channel");
}
}
});
handles.push(handle);
// Every 10 files, yield to allow progress updates
if manifest_idx % 10 == 0 {
// We would update progress here in a full implementation
tokio::task::yield_now().await;
}
}
// Drop the original sender so the receiver knows when we're done
drop(tx);
// Spawn a task to collect all the results
let receiver_task = tokio::spawn(async move {
let mut results = Vec::new();
while let Some(game) = rx.recv().await {
info!("Found game: {} ({})", game.title, game.id);
info!(" Path: {}", game.path.display());
info!(
" Status: Native={}, Cream={}, Smoke={}",
game.native, game.cream_installed, game.smoke_installed
);
// Log Steam API DLLs if any
if !game.api_files.is_empty() {
info!(" Steam API files:");
for api_file in &game.api_files {
info!(" - {}", api_file);
}
}
results.push(game);
}
results
});
// Wait for all scan tasks to complete but don't wait for the results yet
for handle in handles {
// Ignore errors the receiver task will just get fewer results
let _ = handle.await;
}
// Now wait for all results to be collected
if let Ok(results) = receiver_task.await {
games = results;
}
info!("Found {} installed games", games.len());
games
}

View File

@@ -10,11 +10,7 @@
"active": true,
"targets": "all",
"category": "Utility",
"icon": [
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.png"
]
"icon": ["icons/128x128.png", "icons/128x128@2x.png", "icons/icon.png"]
},
"productName": "Creamlinux",
"mainBinaryName": "creamlinux",