aboutsummaryrefslogtreecommitdiff
path: root/libs/nak/src/steam
diff options
context:
space:
mode:
Diffstat (limited to 'libs/nak/src/steam')
-rw-r--r--libs/nak/src/steam/mod.rs135
-rw-r--r--libs/nak/src/steam/paths.rs259
-rw-r--r--libs/nak/src/steam/proton.rs252
3 files changed, 0 insertions, 646 deletions
diff --git a/libs/nak/src/steam/mod.rs b/libs/nak/src/steam/mod.rs
deleted file mode 100644
index c5964aa..0000000
--- a/libs/nak/src/steam/mod.rs
+++ /dev/null
@@ -1,135 +0,0 @@
-//! Steam integration module
-//!
-//! Handles Proton detection, Steam path detection, and mount point discovery.
-//! Shortcuts and config.vdf manipulation removed (handled by C++ side).
-
-mod paths;
-mod proton;
-
-// Re-export path detection utilities
-pub use paths::{
- detect_steam_path_checked, find_steam_path, find_userdata_path,
- get_steam_accounts,
-};
-
-// Re-export Proton detection
-pub use proton::{find_steam_protons, SteamProton};
-
-use std::fs;
-
-/// Kill Steam process gracefully, then force if needed
-pub fn kill_steam() -> Result<(), Box<dyn std::error::Error>> {
- use std::process::Command;
-
- // Try steam -shutdown first (graceful)
- let _ = Command::new("steam")
- .arg("-shutdown")
- .status();
-
- std::thread::sleep(std::time::Duration::from_secs(2));
-
- // Then force kill if still running
- let _ = Command::new("pkill")
- .arg("-9")
- .arg("steam")
- .status();
-
- // Brief wait for Steam to fully exit
- std::thread::sleep(std::time::Duration::from_secs(2));
-
- Ok(())
-}
-
-/// Start Steam in background
-pub fn start_steam() -> Result<(), Box<dyn std::error::Error>> {
- use std::process::{Command, Stdio};
-
- Command::new("setsid")
- .arg("steam")
- .arg("-silent")
- .stdout(Stdio::null())
- .stderr(Stdio::null())
- .spawn()?;
-
- Ok(())
-}
-
-/// Restart Steam (kill then start)
-pub fn restart_steam() -> Result<(), Box<dyn std::error::Error>> {
- kill_steam()?;
- start_steam()?;
- Ok(())
-}
-
-// ============================================================================
-// STEAM_COMPAT_MOUNTS Detection
-// ============================================================================
-
-/// Directories that pressure-vessel already exposes by default
-const ALREADY_EXPOSED: &[&str] = &[
- "bin", "etc", "home", "lib", "lib32", "lib64",
- "overrides", "run", "sbin", "tmp", "usr", "var",
-];
-
-/// System directories that shouldn't be mounted
-const SYSTEM_DIRS: &[&str] = &[
- "proc", "sys", "dev", "boot", "root", "lost+found", "snap",
-];
-
-/// Detect directories at root that need to be added to STEAM_COMPAT_MOUNTS
-pub fn detect_extra_mounts() -> Vec<String> {
- let mut mounts = Vec::new();
-
- let Ok(entries) = fs::read_dir("/") else {
- return mounts;
- };
-
- for entry in entries.flatten() {
- let name = entry.file_name().to_string_lossy().to_string();
-
- if ALREADY_EXPOSED.contains(&name.as_str()) {
- continue;
- }
-
- if SYSTEM_DIRS.contains(&name.as_str()) {
- continue;
- }
-
- if name.starts_with('.') {
- continue;
- }
-
- if entry.path().is_dir() {
- mounts.push(format!("/{}", name));
- }
- }
-
- mounts.sort();
- mounts
-}
-
-/// Generate launch options string with DXVK config file and STEAM_COMPAT_MOUNTS
-pub fn generate_launch_options(dxvk_conf_path: Option<&std::path::Path>, is_electron_app: bool) -> String {
- let mounts = detect_extra_mounts();
-
- let dxvk_part = match dxvk_conf_path {
- Some(path) => format!(
- "DXVK_CONFIG_FILE=\"{}\"",
- crate::config::normalize_path_for_steam(&path.to_string_lossy())
- ),
- None => String::new(),
- };
-
- let electron_flags = if is_electron_app {
- " --disable-gpu --no-sandbox"
- } else {
- ""
- };
-
- match (dxvk_part.is_empty(), mounts.is_empty()) {
- (true, true) => format!("%command%{}", electron_flags),
- (true, false) => format!("STEAM_COMPAT_MOUNTS={} %command%{}", mounts.join(":"), electron_flags),
- (false, true) => format!("{} %command%{}", dxvk_part, electron_flags),
- (false, false) => format!("{} STEAM_COMPAT_MOUNTS={} %command%{}", dxvk_part, mounts.join(":"), electron_flags),
- }
-}
diff --git a/libs/nak/src/steam/paths.rs b/libs/nak/src/steam/paths.rs
deleted file mode 100644
index 8ef5013..0000000
--- a/libs/nak/src/steam/paths.rs
+++ /dev/null
@@ -1,259 +0,0 @@
-//! Steam path detection utilities
-
-use std::fs;
-use std::path::PathBuf;
-
-use crate::logging::{log_info, log_warning};
-
-// ============================================================================
-// Core Path Detection
-// ============================================================================
-
-/// Find the Steam installation path.
-#[must_use]
-pub fn find_steam_path() -> Option<PathBuf> {
- let home = std::env::var("HOME").ok()?;
-
- let steam_paths = [
- format!("{}/.steam/steam", home),
- format!("{}/.local/share/Steam", home),
- format!("{}/.var/app/com.valvesoftware.Steam/.steam/steam", home),
- format!("{}/snap/steam/common/.steam/steam", home),
- ];
-
- steam_paths
- .iter()
- .map(PathBuf::from)
- .find(|p| p.exists())
-}
-
-/// Find the Steam userdata directory.
-#[must_use]
-pub fn find_userdata_path() -> Option<PathBuf> {
- let config = crate::config::AppConfig::load();
- if !config.selected_steam_account.is_empty() {
- if let Some(path) = find_userdata_path_for_account(&config.selected_steam_account) {
- return Some(path);
- }
- }
-
- let steam_path = find_steam_path()?;
- let userdata = steam_path.join("userdata");
-
- if !userdata.exists() {
- return None;
- }
-
- let accounts = get_steam_accounts();
-
- if let Some(most_recent) = accounts.iter().find(|a| a.most_recent) {
- let path = userdata.join(&most_recent.account_id);
- if path.exists() {
- log_info(&format!(
- "Using Steam account from loginusers.vdf (MostRecent): {} ({})",
- most_recent.persona_name, most_recent.account_id
- ));
- return Some(path);
- }
- }
-
- if let Some(first_account) = accounts.first() {
- let path = userdata.join(&first_account.account_id);
- if path.exists() {
- log_info(&format!(
- "Using Steam account from loginusers.vdf (most recent timestamp): {} ({})",
- first_account.persona_name, first_account.account_id
- ));
- return Some(path);
- }
- }
-
- log_warning("Could not determine active Steam account from loginusers.vdf, falling back to directory modification time");
-
- let mut user_dirs: Vec<PathBuf> = Vec::new();
-
- if let Ok(entries) = fs::read_dir(&userdata) {
- for entry in entries.flatten() {
- let path = entry.path();
- if path.is_dir() {
- if let Some(name) = path.file_name() {
- let name_str = name.to_string_lossy();
- if name_str != "0" && name_str.chars().all(|c| c.is_ascii_digit()) {
- user_dirs.push(path);
- }
- }
- }
- }
- }
-
- user_dirs.sort_by(|a, b| {
- let a_time = fs::metadata(a).and_then(|m| m.modified()).ok();
- let b_time = fs::metadata(b).and_then(|m| m.modified()).ok();
- b_time.cmp(&a_time)
- });
-
- user_dirs.into_iter().next()
-}
-
-// ============================================================================
-// Steam Account Detection (loginusers.vdf parsing)
-// ============================================================================
-
-/// Information about a Steam user account
-#[derive(Debug, Clone)]
-pub struct SteamAccount {
- pub account_id: String,
- pub persona_name: String,
- pub most_recent: bool,
- pub timestamp: u64,
-}
-
-/// Get all Steam accounts from loginusers.vdf with their display names
-#[must_use]
-pub fn get_steam_accounts() -> Vec<SteamAccount> {
- let Some(steam_path) = find_steam_path() else {
- return Vec::new();
- };
-
- let loginusers_path = steam_path.join("config/loginusers.vdf");
- let userdata_path = steam_path.join("userdata");
-
- let Ok(content) = fs::read_to_string(&loginusers_path) else {
- return Vec::new();
- };
-
- let mut accounts = Vec::new();
-
- let mut current_steam_id: Option<String> = None;
- let mut current_account: Option<SteamAccountBuilder> = None;
-
- for line in content.lines() {
- let trimmed = line.trim();
-
- if trimmed.starts_with('"') && trimmed.ends_with('"') {
- let id = trimmed.trim_matches('"');
- if id.len() == 17 && id.starts_with("7656") && id.chars().all(|c| c.is_ascii_digit()) {
- if let (Some(steam_id), Some(builder)) = (current_steam_id.take(), current_account.take()) {
- if let Some(account) = builder.build(&steam_id, &userdata_path) {
- accounts.push(account);
- }
- }
- current_steam_id = Some(id.to_string());
- current_account = Some(SteamAccountBuilder::default());
- }
- }
-
- if let Some(ref mut builder) = current_account {
- if let Some((key, value)) = parse_vdf_kv(trimmed) {
- match key.to_lowercase().as_str() {
- "accountname" => builder.account_name = Some(value),
- "personaname" => builder.persona_name = Some(value),
- "mostrecent" => builder.most_recent = value == "1",
- "timestamp" => builder.timestamp = value.parse().unwrap_or(0),
- _ => {}
- }
- }
- }
- }
-
- if let (Some(steam_id), Some(builder)) = (current_steam_id, current_account) {
- if let Some(account) = builder.build(&steam_id, &userdata_path) {
- accounts.push(account);
- }
- }
-
- accounts.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
-
- accounts
-}
-
-#[derive(Default)]
-struct SteamAccountBuilder {
- account_name: Option<String>,
- persona_name: Option<String>,
- most_recent: bool,
- timestamp: u64,
-}
-
-impl SteamAccountBuilder {
- fn build(self, steam_id: &str, userdata_base: &std::path::Path) -> Option<SteamAccount> {
- let account_name = self.account_name?;
- let persona_name = self.persona_name.unwrap_or_else(|| account_name.clone());
-
- let steam64: u64 = steam_id.parse().ok()?;
- let account_id = (steam64 - 76561197960265728).to_string();
-
- let userdata_path = userdata_base.join(&account_id);
-
- if !userdata_path.exists() {
- return None;
- }
-
- Some(SteamAccount {
- account_id,
- persona_name,
- most_recent: self.most_recent,
- timestamp: self.timestamp,
- })
- }
-}
-
-/// Parse a VDF key-value pair like: "Key" "Value"
-fn parse_vdf_kv(line: &str) -> Option<(String, String)> {
- let mut parts = Vec::new();
- let mut current = String::new();
- let mut in_quotes = false;
-
- for c in line.chars() {
- match c {
- '"' => {
- if in_quotes {
- parts.push(current.clone());
- current.clear();
- }
- in_quotes = !in_quotes;
- }
- _ if in_quotes => current.push(c),
- _ => {}
- }
- }
-
- if parts.len() >= 2 {
- Some((parts[0].clone(), parts[1].clone()))
- } else {
- None
- }
-}
-
-/// Find the userdata path for a specific Steam account
-#[must_use]
-pub fn find_userdata_path_for_account(account_id: &str) -> Option<PathBuf> {
- let steam_path = find_steam_path()?;
- let userdata = steam_path.join("userdata").join(account_id);
-
- if userdata.exists() {
- Some(userdata)
- } else {
- None
- }
-}
-
-// ============================================================================
-// Convenience Wrappers
-// ============================================================================
-
-/// Detect the Steam installation path with logging.
-#[must_use]
-pub fn detect_steam_path_checked() -> Option<String> {
- match find_steam_path() {
- Some(path) => {
- let path_str = path.to_string_lossy().to_string();
- log_info(&format!("Steam detected at: {}", path_str));
- Some(path_str)
- }
- None => {
- log_warning("Steam installation not detected! NaK requires Steam to be installed.");
- None
- }
- }
-}
diff --git a/libs/nak/src/steam/proton.rs b/libs/nak/src/steam/proton.rs
deleted file mode 100644
index 4059dc4..0000000
--- a/libs/nak/src/steam/proton.rs
+++ /dev/null
@@ -1,252 +0,0 @@
-//! Steam Proton detection and management
-//!
-//! Finds Protons that Steam can see and use for non-Steam games.
-//! This includes Steam's built-in Protons and custom Protons in compatibilitytools.d.
-
-use std::fs;
-use std::path::PathBuf;
-
-use super::find_steam_path;
-
-/// Information about an installed Proton version
-#[derive(Debug, Clone)]
-pub struct SteamProton {
- /// Display name (e.g., "GE-Proton9-20", "Proton Experimental")
- pub name: String,
- /// Internal name used in config.vdf (e.g., "proton_experimental", "GE-Proton9-20")
- pub config_name: String,
- /// Full path to the Proton installation
- pub path: PathBuf,
- /// Whether this is a Steam-provided Proton (vs custom)
- pub is_steam_proton: bool,
- /// Whether this is Proton Experimental
- pub is_experimental: bool,
-}
-
-impl SteamProton {
- /// Get the path to the wine binary.
- pub fn wine_binary(&self) -> Option<PathBuf> {
- let paths = [
- self.path.join("files/bin/wine"),
- self.path.join("dist/bin/wine"),
- ];
- paths.into_iter().find(|p| p.exists())
- }
-
- /// Get the path to the wineserver binary.
- pub fn wineserver_binary(&self) -> Option<PathBuf> {
- let paths = [
- self.path.join("files/bin/wineserver"),
- self.path.join("dist/bin/wineserver"),
- ];
- paths.into_iter().find(|p| p.exists())
- }
-
- /// Get the bin directory containing wine executables.
- pub fn bin_dir(&self) -> Option<PathBuf> {
- self.wine_binary().and_then(|p| p.parent().map(|p| p.to_path_buf()))
- }
-}
-
-/// Find all Protons that Steam can use (Proton 10+ only)
-pub fn find_steam_protons() -> Vec<SteamProton> {
- let mut protons = Vec::new();
-
- let Some(steam_path) = find_steam_path() else {
- return protons;
- };
-
- // 1. Steam's built-in Protons (steamapps/common/Proton*)
- protons.extend(find_builtin_protons(&steam_path));
-
- // 2. Custom Protons in user's compatibilitytools.d
- protons.extend(find_custom_protons(&steam_path));
-
- // 3. System-level Protons in /usr/share/steam/compatibilitytools.d/
- // (Arch packages Proton here; Flatpak has --filesystem=/usr/share/steam:ro)
- protons.extend(find_system_protons());
-
- // Filter to only include Proton 10+ (required for Steam-native integration)
- protons.retain(is_proton_10_or_newer);
-
- // Filter to only include Protons with valid wine binaries
- protons.retain(|p| {
- let has_wine = p.wine_binary().is_some();
- if !has_wine {
- crate::logging::log_warning(&format!(
- "Skipping Proton '{}': wine binary not found at expected paths (files/bin/wine or dist/bin/wine)",
- p.name
- ));
- }
- has_wine
- });
-
- // Sort: Experimental first, then by name descending (newest first)
- protons.sort_by(|a, b| {
- if a.is_experimental != b.is_experimental {
- return b.is_experimental.cmp(&a.is_experimental);
- }
- b.name.cmp(&a.name)
- });
-
- protons
-}
-
-/// Check if a Proton version is 10 or newer
-fn is_proton_10_or_newer(proton: &SteamProton) -> bool {
- let name = &proton.name;
-
- if proton.is_experimental || name.contains("Experimental") {
- return true;
- }
-
- if name.contains("CachyOS") {
- return true;
- }
-
- if name == "LegacyRuntime" || name.contains("Runtime") {
- return false;
- }
-
- if name.starts_with("GE-Proton") {
- if let Some(version_part) = name.strip_prefix("GE-Proton") {
- let major: Option<u32> = version_part
- .split('-')
- .next()
- .and_then(|s| s.parse().ok());
- return major.map(|v| v >= 10).unwrap_or(false);
- }
- }
-
- if name.starts_with("Proton ") {
- if let Some(version_part) = name.strip_prefix("Proton ") {
- let major: Option<u32> = version_part
- .split('.')
- .next()
- .and_then(|s| s.parse().ok());
- return major.map(|v| v >= 10).unwrap_or(false);
- }
- }
-
- if name.starts_with("EM-") {
- if let Some(version_part) = name.strip_prefix("EM-") {
- let major: Option<u32> = version_part
- .split('.')
- .next()
- .and_then(|s| s.parse().ok());
- return major.map(|v| v >= 10).unwrap_or(false);
- }
- }
-
- // Unknown format - allow it
- true
-}
-
-/// Find Steam's built-in Proton versions
-fn find_builtin_protons(steam_path: &std::path::Path) -> Vec<SteamProton> {
- let mut found = Vec::new();
- let common_dir = steam_path.join("steamapps/common");
-
- let Ok(entries) = fs::read_dir(&common_dir) else {
- return found;
- };
-
- for entry in entries.flatten() {
- let path = entry.path();
- if !path.is_dir() {
- continue;
- }
-
- let name = entry.file_name().to_string_lossy().to_string();
-
- if name.starts_with("Proton") && path.join("proton").exists() {
- let is_experimental = name.contains("Experimental");
-
- let config_name = if is_experimental {
- "proton_experimental".to_string()
- } else {
- let version = name.replace("Proton ", "");
- let major = version.split('.').next().unwrap_or(&version);
- format!("proton_{}", major)
- };
-
- found.push(SteamProton {
- name: name.clone(),
- config_name,
- path,
- is_steam_proton: true,
- is_experimental,
- });
- }
- }
-
- found
-}
-
-/// Find custom Protons in compatibilitytools.d
-fn find_custom_protons(steam_path: &std::path::Path) -> Vec<SteamProton> {
- let mut found = Vec::new();
- let compat_dir = steam_path.join("compatibilitytools.d");
-
- let Ok(entries) = fs::read_dir(&compat_dir) else {
- return found;
- };
-
- for entry in entries.flatten() {
- let path = entry.path();
- if !path.is_dir() {
- continue;
- }
-
- let name = entry.file_name().to_string_lossy().to_string();
-
- let has_proton = path.join("proton").exists();
- let has_vdf = path.join("compatibilitytool.vdf").exists();
-
- if has_proton || has_vdf {
- found.push(SteamProton {
- name: name.clone(),
- config_name: name.clone(),
- path,
- is_steam_proton: false,
- is_experimental: false,
- });
- }
- }
-
- found
-}
-
-/// Find system-level Protons in /usr/share/steam/compatibilitytools.d/
-fn find_system_protons() -> Vec<SteamProton> {
- let mut found = Vec::new();
- let system_compat_dir = PathBuf::from("/usr/share/steam/compatibilitytools.d");
-
- let Ok(entries) = fs::read_dir(&system_compat_dir) else {
- return found;
- };
-
- for entry in entries.flatten() {
- let path = entry.path();
- if !path.is_dir() {
- continue;
- }
-
- let name = entry.file_name().to_string_lossy().to_string();
-
- let has_proton = path.join("proton").exists();
- let has_vdf = path.join("compatibilitytool.vdf").exists();
-
- if has_proton || has_vdf {
- found.push(SteamProton {
- name: name.clone(),
- config_name: name.clone(),
- path,
- is_steam_proton: false,
- is_experimental: false,
- });
- }
- }
-
- found
-}