Files
cstb-next/src-tauri/src/cmds.rs
2025-11-08 18:16:24 +08:00

849 lines
28 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::steam;
use crate::tool::*;
use crate::tool::updater::{check_update, download_update, install_update, UpdateInfo};
use crate::vdf::preset;
use crate::vdf::preset::VideoConfig;
use crate::wrap_err;
use anyhow::Result;
use std::fs::File;
use std::fs;
use std::path::Path;
use std::io::{BufRead, BufReader};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
use tauri::path::BaseDirectory;
use tauri::Manager;
use serde::{Deserialize, Serialize};
// use tauri_plugin_shell::ShellExt;
// pub type Result<T, String = ()> = Result<T, String>;
// 全局下载取消标志
static DOWNLOAD_CANCELLED: OnceLock<Arc<AtomicBool>> = OnceLock::new();
fn get_download_cancelled() -> Arc<AtomicBool> {
DOWNLOAD_CANCELLED.get_or_init(|| Arc::new(AtomicBool::new(false))).clone()
}
#[tauri::command]
pub fn greet(name: &str) -> Result<String, String> {
Ok(format!("Hello, {}! You've been greeted from Rust!", name))
}
// 工具
#[tauri::command]
pub fn launch_game(steam_path: &str, launch_option: &str, server: &str) -> Result<String, String> {
println!(
"{}: launching game on server: {}, with launch Option {}",
steam_path, server, launch_option
);
// wrap_err!(steam::launch_game(steam_path, launch_option, server));
// 如果有错误,打印出来
if let Err(e) = steam::launch_game(steam_path, launch_option, server) {
println!("Error: {}", e);
return Err(e.to_string());
}
// steam::launch_game(steam_path, launch_option, server);
Ok(format!(
"Launching game on server: {}, with launch Option {}",
server, launch_option
))
}
#[tauri::command]
pub fn kill_game() -> Result<String, String> {
Ok(common::kill("cs2.exe"))
}
#[tauri::command]
pub fn check_process_running(process_name: &str) -> Result<bool, String> {
Ok(common::check_process_running(process_name))
}
#[tauri::command]
pub fn kill_steam() -> Result<String, String> {
Ok(common::kill("steam.exe"))
}
// Steam
#[tauri::command]
pub fn get_steam_path() -> Result<String, String> {
wrap_err!(steam::path::get_steam_path())
}
#[tauri::command]
pub fn get_cs_path(name: &str, steam_dir: &str) -> Result<String, String> {
wrap_err!(steam::path::get_cs_path(name, steam_dir))
}
#[tauri::command]
pub fn open_path(path: &str) -> Result<(), String> {
wrap_err!(common::open_path(path))
}
#[tauri::command]
pub fn get_powerplan() -> Result<i32, String> {
#[cfg(target_os = "windows")]
let powerplan = powerplan::get_powerplan()?;
#[cfg(not(target_os = "windows"))]
let powerplan = powerplan::PowerPlanMode::Other as i32;
Ok(powerplan)
}
#[tauri::command]
pub fn set_powerplan(plan: i32) -> Result<(), String> {
#[cfg(target_os = "windows")]
powerplan::set_powerplan(plan)?;
Ok(())
}
#[tauri::command]
pub fn get_steam_users(steam_dir: &str) -> Result<Vec<preset::User>, String> {
wrap_err!(preset::get_users(steam_dir))
}
#[tauri::command]
pub fn set_auto_login_user(user: &str) -> Result<String, String> {
#[cfg(target_os = "windows")]
steam::reg::set_auto_login_user(user)?;
Ok(format!("Set auto login user to {}", user))
}
#[tauri::command]
pub fn start_watch_loginusers(app: tauri::AppHandle, steam_dir: String) -> Result<(), String> {
wrap_err!(steam::watch::start_watch_loginusers(app, steam_dir))
}
#[tauri::command]
pub fn start_watch_cs2_video(
app: tauri::AppHandle,
steam_dir: String,
steam_id32: u32,
) -> Result<(), String> {
wrap_err!(steam::watch::start_watch_cs2_video(app, steam_dir, steam_id32))
}
#[tauri::command]
pub fn stop_watch_cs2_video() -> Result<(), String> {
steam::watch::stop_watch_cs2_video();
Ok(())
}
#[tauri::command]
pub fn get_cs2_video_config(steam_dir: &str, steam_id32: u32) -> Result<VideoConfig, String> {
let p = format!(
"{}/userdata/{}/730/local/cfg/cs2_video.txt",
steam_dir, steam_id32
);
let video = preset::get_cs2_video(p.as_str()).map_err(|e| e.to_string())?;
Ok(video)
}
#[tauri::command]
pub fn set_cs2_video_config(
steam_dir: &str,
steam_id32: u32,
video_config: VideoConfig,
) -> Result<(), String> {
let p = format!(
"{}/userdata/{}/730/local/cfg/cs2_video.txt",
steam_dir, steam_id32
);
preset::set_cs2_video(p.as_str(), video_config).map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub fn check_path(path: &str) -> Result<bool, String> {
Ok(std::path::Path::new(&path).exists())
}
#[tauri::command]
pub fn check_steam_dir_valid(steam_dir: &str) -> Result<bool, String> {
use std::path::Path;
let path = Path::new(steam_dir);
if !path.exists() {
return Ok(false);
}
// 检查是否存在 steam.exe 或 config 目录(至少有一个即可)
let steam_exe = path.join("steam.exe");
let config_dir = path.join("config");
Ok(steam_exe.exists() || config_dir.exists())
}
///// 录像
#[tauri::command]
pub async fn analyze_replay(app: tauri::AppHandle, path: &str) -> Result<String, String> {
// 检测文件是否存在
if !std::path::Path::new(&path).exists() {
return Err("文件不存在".to_string());
}
// 获取应用配置目录
let config_dir = app
.path()
.resolve("metadata", BaseDirectory::AppConfig)
.expect("无法获取配置目录");
// 确保 metadata 文件夹存在
if !config_dir.exists() {
fs::create_dir_all(&config_dir).expect("无法创建 metadata 文件夹");
}
// 提取文件名部分
let file_name = std::path::Path::new(path)
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("default_filename");
// 拼接输出文件路径
let output_path = config_dir.join(format!("{}.json", file_name));
// 确保输出文件存在,如果不存在则创建空文件
if !output_path.exists() {
File::create(&output_path).expect("无法创建输出文件");
}
// 调用项目绑定cli程序
let cli_path = app
.path()
.resolve("resources/csda", BaseDirectory::Resource)
.expect("analyzer not found");
println!("cli path: {}", cli_path.display());
let output = std::process::Command::new(cli_path)
.arg("-demo-path")
.arg(path)
.arg("-format")
.arg("json")
.arg("-minify")
.arg("-output")
.arg(output_path.to_str().expect("路径转换失败"))
.output()
.expect("Failed to execute command");
// 获取输出
let output_str = String::from_utf8_lossy(&output.stdout);
// 打印输出
println!("{}", output_str);
// 返回结果
Ok(output_str.to_string())
}
// 帧数测试相关
#[tauri::command]
pub fn get_console_log_path(cs_path: &str) -> Result<String, String> {
// cs_path 是类似 "game\bin\win64" 的路径,需要向上找到 game\csgo\console.log
let path = Path::new(cs_path);
// 向上找到 game 目录
if let Some(game_dir) = path.ancestors().find(|p| {
p.file_name()
.and_then(|n| n.to_str())
.map(|n| n == "game")
.unwrap_or(false)
}) {
let console_log_path = game_dir.join("csgo").join("console.log");
Ok(console_log_path.to_string_lossy().to_string())
} else {
Err("无法找到 game 目录".to_string())
}
}
#[tauri::command]
pub fn read_vprof_report(console_log_path: &str) -> Result<String, String> {
let path = Path::new(console_log_path);
if !path.exists() {
return Err("console.log 文件不存在".to_string());
}
let file = File::open(path).map_err(|e| format!("无法打开文件: {}", e))?;
let reader = BufReader::new(file);
let mut vprof_lines = Vec::new();
let mut in_vprof_section = false;
let mut empty_line_count = 0;
for line_result in reader.lines() {
let line = line_result.map_err(|e| format!("读取行错误: {}", e))?;
// 检测 [VProf] 标记
if line.contains("[VProf]") {
in_vprof_section = true;
empty_line_count = 0;
vprof_lines.push(line.clone());
} else if in_vprof_section {
// 如果在 VProf 部分中
if line.trim().is_empty() {
empty_line_count += 1;
// 如果遇到两个连续的空行,结束 VProf 部分
if empty_line_count >= 2 {
break;
}
vprof_lines.push(line.clone());
} else {
empty_line_count = 0;
vprof_lines.push(line.clone());
}
}
}
if vprof_lines.is_empty() {
return Err("未找到 [VProf] 报告".to_string());
}
Ok(vprof_lines.join("\n"))
}
// 更新相关命令
/// 检查更新(支持 GitHub Release 和自定义端点)
#[tauri::command]
pub async fn check_app_update(
app: tauri::AppHandle,
endpoint: Option<String>,
use_mirror: Option<bool>,
include_prerelease: Option<bool>,
) -> Result<Option<UpdateInfo>, String> {
let current_version = app.package_info().version.to_string();
let use_mirror = use_mirror.unwrap_or(false);
let include_prerelease = include_prerelease.unwrap_or(false);
// println!("[检查更新命令] 当前应用版本: {}", current_version);
// println!("[检查更新命令] 使用镜像: {}", use_mirror);
// println!("[检查更新命令] 包含预发布版本: {}", include_prerelease);
// if let Some(ref ep) = endpoint {
// println!("[检查更新命令] 自定义端点: {}", ep);
// }
// 从环境变量获取 GitHub 仓库信息,如果没有则使用默认值
const DEFAULT_GITHUB_REPO: &str = "plsgo/cstb";
let github_repo_str = std::env::var("GITHUB_REPO").ok();
let github_repo = github_repo_str.as_deref().unwrap_or(DEFAULT_GITHUB_REPO);
// println!("[检查更新命令] GitHub 仓库: {}", github_repo);
let result = wrap_err!(check_update(
endpoint.as_deref(),
&current_version,
use_mirror,
Some(github_repo),
include_prerelease
).await)?;
// if result.is_some() {
// println!("[检查更新命令] ✓ 返回更新信息");
// } else {
// println!("[检查更新命令] ✗ 无更新可用");
// }
Ok(result)
}
/// 下载更新
#[tauri::command]
pub async fn download_app_update(
app: tauri::AppHandle,
download_url: String,
) -> Result<String, String> {
// 重置取消标志
let cancelled = get_download_cancelled();
cancelled.store(false, Ordering::Relaxed);
let path = wrap_err!(download_update(&app, &download_url, cancelled).await)?;
Ok(path.to_string_lossy().to_string())
}
/// 取消下载
#[tauri::command]
pub fn cancel_download_update() -> Result<(), String> {
let cancelled = get_download_cancelled();
cancelled.store(true, Ordering::Relaxed);
Ok(())
}
/// 安装更新
#[tauri::command]
pub fn install_app_update(installer_path: String) -> Result<(), String> {
wrap_err!(install_update(&installer_path))
}
/// 获取 PowerShell Get-ComputerInfo 信息(异步版本)
#[tauri::command]
#[cfg(target_os = "windows")]
pub async fn get_computer_info() -> Result<serde_json::Value, String> {
use tokio::process::Command;
// 异步执行 PowerShell 命令获取计算机信息并转换为 JSON
let output = Command::new("powershell")
.args(&[
"-NoProfile",
"-Command",
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-ComputerInfo | Select-Object OsName, OSDisplayVersion, BiosSMBIOSBIOSVersion, CsManufacturer, CsName | ConvertTo-Json -Compress"
])
.output()
.await
.map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("PowerShell 命令执行失败: {}", stderr));
}
// 处理 PowerShell 输出,移除 BOM 和空白字符
let stdout = String::from_utf8_lossy(&output.stdout);
let cleaned = stdout.trim().trim_start_matches('\u{feff}'); // 移除 BOM
// 如果输出为空,返回空对象
if cleaned.is_empty() {
return Ok(serde_json::json!({}));
}
let mut json: serde_json::Value = serde_json::from_str(cleaned)
.map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?;
// 对于 Windows 11优先使用 OSDisplayVersion 获取版本代码(如 25H2
// 如果 OSDisplayVersion 存在且包含版本代码,提取它
if let Some(os_display_version) = json.get("OSDisplayVersion").and_then(|v| v.as_str()) {
// OSDisplayVersion 格式可能是 "25H2" 或 "Windows 11 版本 25H2" 等
// 尝试提取版本代码(如 25H2
if let Some(capture) = regex::Regex::new(r"(\d+H\d+)")
.ok()
.and_then(|re| re.captures(os_display_version))
{
if let Some(version_code) = capture.get(1) {
json["ReleaseId"] = serde_json::Value::String(version_code.as_str().to_string());
}
}
}
// 如果没有从 OSDisplayVersion 获取到版本代码,尝试从注册表获取 ReleaseId
if !json.get("ReleaseId").and_then(|v| v.as_str()).is_some() {
let release_id_output = Command::new("powershell")
.args(&[
"-NoProfile",
"-Command",
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; try { (Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion').ReleaseId } catch { $null }"
])
.output()
.await;
if let Ok(release_output) = release_id_output {
if release_output.status.success() {
let release_str = String::from_utf8_lossy(&release_output.stdout).trim().to_string();
if !release_str.is_empty() {
json["ReleaseId"] = serde_json::Value::String(release_str);
}
}
}
}
Ok(json)
}
/// 获取 PowerShell Get-ComputerInfo 信息(非 Windows 平台返回空对象)
#[tauri::command]
#[cfg(not(target_os = "windows"))]
pub async fn get_computer_info() -> Result<serde_json::Value, String> {
Ok(serde_json::json!({}))
}
/// GPU 信息结构体
#[derive(Debug, Serialize, Deserialize)]
pub struct GpuInfo {
vendor: String,
model: String,
family: String,
device_id: String,
total_vram: u64,
used_vram: u64,
load_pct: u32,
temperature: f64,
}
/// 辅助函数:格式化字节数
fn format_bytes(bytes: u64) -> String {
let gb = bytes as f64 / 1024.0 / 1024.0 / 1024.0;
if gb >= 1.0 {
format!("{:.2}GB", gb)
} else {
let mb = bytes as f64 / 1024.0 / 1024.0;
format!("{:.2}MB", mb)
}
}
/// 获取 GPU 信息
#[tauri::command]
pub fn get_gpu_info() -> Result<Option<GpuInfo>, String> {
use gfxinfo::active_gpu;
match active_gpu() {
Ok(gpu) => {
let info = gpu.info();
let temp = info.temperature() as f64 / 1000.0;
Ok(Some(GpuInfo {
vendor: gpu.vendor().to_string(),
model: gpu.model().to_string(),
family: gpu.family().to_string(),
device_id: gpu.device_id().to_string(),
total_vram: info.total_vram(),
used_vram: info.used_vram(),
load_pct: info.load_pct(),
temperature: temp,
}))
}
Err(e) => {
println!("✗ GPU 信息获取失败: {}", e);
Ok(None)
}
}
}
/// 内存信息结构体
#[derive(Debug, Serialize, Deserialize)]
pub struct MemoryInfo {
#[serde(skip_serializing_if = "Option::is_none")]
capacity: Option<u64>, // 容量(字节)
#[serde(skip_serializing_if = "Option::is_none")]
manufacturer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
speed: Option<u32>, // MHz实际频率 ConfiguredClockSpeed
#[serde(skip_serializing_if = "Option::is_none")]
default_speed: Option<u32>, // MHz默认频率 Speed如果存在
}
/// 显示器信息结构体
#[derive(Debug, Serialize, Deserialize)]
pub struct MonitorInfo {
#[serde(skip_serializing_if = "Option::is_none")]
manufacturer: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
model: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
refresh_rate: Option<u32>, // Hz
#[serde(skip_serializing_if = "Option::is_none")]
resolution_width: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
resolution_height: Option<u32>,
}
/// 主板信息结构体
#[derive(Debug, Serialize, Deserialize)]
pub struct MotherboardInfo {
#[serde(skip_serializing_if = "Option::is_none")]
manufacturer: Option<String>, // 制造商
#[serde(skip_serializing_if = "Option::is_none")]
model: Option<String>, // 型号
#[serde(skip_serializing_if = "Option::is_none")]
version: Option<String>,
}
/// 获取内存信息Windows
#[tauri::command]
#[cfg(target_os = "windows")]
pub async fn get_memory_info() -> Result<Vec<MemoryInfo>, String> {
use tokio::process::Command;
// 执行 PowerShell 命令获取内存信息
let output = Command::new("powershell")
.args(&[
"-NoProfile",
"-Command",
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-WmiObject Win32_PhysicalMemory | Select-Object Capacity, Manufacturer, ConfiguredClockSpeed, Speed | ConvertTo-Json -Compress"
])
.output()
.await
.map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("PowerShell 命令执行失败: {}", stderr));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let cleaned = stdout.trim().trim_start_matches('\u{feff}');
if cleaned.is_empty() {
return Ok(vec![]);
}
// PowerShell 可能返回数组或单个对象
let json: serde_json::Value = serde_json::from_str(cleaned)
.map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?;
let mut memory_list = Vec::new();
if let Some(array) = json.as_array() {
// 如果是数组
for item in array {
memory_list.push(parse_memory_info(item));
}
} else {
// 如果是单个对象
memory_list.push(parse_memory_info(&json));
}
Ok(memory_list)
}
/// 解析内存信息
fn parse_memory_info(json: &serde_json::Value) -> MemoryInfo {
// 容量(字节)
let capacity = json.get("Capacity")
.and_then(|v| v.as_u64());
let manufacturer = json.get("Manufacturer")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
// 实际频率:优先使用 ConfiguredClockSpeed
let speed = json.get("ConfiguredClockSpeed")
.and_then(|v| v.as_u64())
.map(|v| v as u32);
// 默认频率Speed如果存在
let default_speed = json.get("Speed")
.and_then(|v| v.as_u64())
.map(|v| v as u32);
MemoryInfo {
capacity,
manufacturer,
speed,
default_speed,
}
}
/// 获取内存信息(非 Windows 平台返回空)
#[tauri::command]
#[cfg(not(target_os = "windows"))]
pub async fn get_memory_info() -> Result<Vec<MemoryInfo>, String> {
Ok(vec![])
}
/// 获取显示器信息Windows
#[tauri::command]
#[cfg(target_os = "windows")]
pub async fn get_monitor_info() -> Result<Vec<MonitorInfo>, String> {
use tokio::process::Command;
// 执行 PowerShell 命令获取显示器信息
let output = Command::new("powershell")
.args(&[
"-NoProfile",
"-Command",
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-CimInstance -Namespace root/wmi -ClassName WmiMonitorID | ForEach-Object { [PSCustomObject]@{ Manufacturer = [System.Text.Encoding]::ASCII.GetString($_.ManufacturerName) -replace \"`0\"; Model = [System.Text.Encoding]::ASCII.GetString($_.UserFriendlyName) -replace \"`0\" } } | ConvertTo-Json -Compress"
])
.output()
.await
.map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("PowerShell 命令执行失败: {}", stderr));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let cleaned = stdout.trim().trim_start_matches('\u{feff}');
if cleaned.is_empty() {
return Ok(vec![]);
}
let json: serde_json::Value = serde_json::from_str(cleaned)
.map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?;
let mut monitor_list = Vec::new();
if let Some(array) = json.as_array() {
for item in array {
monitor_list.push(parse_monitor_info(item));
}
} else {
monitor_list.push(parse_monitor_info(&json));
}
// 尝试获取刷新率和分辨率信息
let _display_output = Command::new("powershell")
.args(&[
"-NoProfile",
"-Command",
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-CimInstance -Namespace root/wmi -ClassName WmiMonitorBasicDisplayParams | Select-Object MaxHorizontalImageSize, MaxVerticalImageSize | ConvertTo-Json -Compress"
])
.output()
.await;
// 获取刷新率信息
let _refresh_output = Command::new("powershell")
.args(&[
"-NoProfile",
"-Command",
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-CimInstance -Namespace root/wmi -ClassName WmiMonitorListedSupportedSourceModes | Select-Object -First 1 -ExpandProperty ModeTimings | Select-Object -First 1 -ExpandProperty RefreshRate | ConvertTo-Json -Compress"
])
.output()
.await;
// 获取当前显示器的分辨率和刷新率
let current_display_output = Command::new("powershell")
.args(&[
"-NoProfile",
"-Command",
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Add-Type -AssemblyName System.Windows.Forms; $screens = [System.Windows.Forms.Screen]::AllScreens; $screens | ForEach-Object { [PSCustomObject]@{ Width = $_.Bounds.Width; Height = $_.Bounds.Height; Primary = $_.Primary } } | ConvertTo-Json -Compress"
])
.output()
.await;
// 合并显示器信息
if let Ok(display_result) = current_display_output {
if display_result.status.success() {
let display_str = String::from_utf8_lossy(&display_result.stdout).to_string();
let display_str = display_str.trim().trim_start_matches('\u{feff}').to_string();
if let Ok(display_json) = serde_json::from_str::<serde_json::Value>(&display_str) {
if let Some(displays) = display_json.as_array() {
for (i, display) in displays.iter().enumerate() {
if i < monitor_list.len() {
if let Some(width) = display.get("Width").and_then(|v| v.as_u64()) {
monitor_list[i].resolution_width = Some(width as u32);
}
if let Some(height) = display.get("Height").and_then(|v| v.as_u64()) {
monitor_list[i].resolution_height = Some(height as u32);
}
}
}
} else if let Some(display) = display_json.as_object() {
if monitor_list.len() > 0 {
if let Some(width) = display.get("Width").and_then(|v| v.as_u64()) {
monitor_list[0].resolution_width = Some(width as u32);
}
if let Some(height) = display.get("Height").and_then(|v| v.as_u64()) {
monitor_list[0].resolution_height = Some(height as u32);
}
}
}
}
}
}
Ok(monitor_list)
}
/// 解析显示器信息
fn parse_monitor_info(json: &serde_json::Value) -> MonitorInfo {
let manufacturer = json.get("Manufacturer")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let model = json.get("Model")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
// 组合制造商和型号作为名称
let name = match (&manufacturer, &model) {
(Some(mfg), Some(model_name)) => Some(format!("{} {}", mfg, model_name)),
(Some(mfg), None) => Some(mfg.clone()),
(None, Some(model_name)) => Some(model_name.clone()),
(None, None) => None,
};
MonitorInfo {
manufacturer,
model,
name,
refresh_rate: None, // 需要从其他来源获取
resolution_width: None,
resolution_height: None,
}
}
/// 获取显示器信息(非 Windows 平台返回空)
#[tauri::command]
#[cfg(not(target_os = "windows"))]
pub async fn get_monitor_info() -> Result<Vec<MonitorInfo>, String> {
Ok(vec![])
}
/// 获取主板信息Windows
#[tauri::command]
#[cfg(target_os = "windows")]
pub async fn get_motherboard_info() -> Result<MotherboardInfo, String> {
use tokio::process::Command;
// 执行 PowerShell 命令获取主板信息
let output = Command::new("powershell")
.args(&[
"-NoProfile",
"-Command",
"[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-CimInstance -ClassName Win32_BaseBoard | Select-Object Manufacturer, Product, Version | ConvertTo-Json -Compress"
])
.output()
.await
.map_err(|e| format!("执行 PowerShell 命令失败: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("PowerShell 命令执行失败: {}", stderr));
}
let stdout = String::from_utf8_lossy(&output.stdout);
let cleaned = stdout.trim().trim_start_matches('\u{feff}');
if cleaned.is_empty() {
return Ok(MotherboardInfo {
manufacturer: None,
model: None,
version: None,
});
}
let json: serde_json::Value = serde_json::from_str(cleaned)
.map_err(|e| format!("解析 JSON 失败: {},原始输出: {}", e, cleaned))?;
// 分别获取制造商和型号
let manufacturer = json.get("Manufacturer")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let model = json.get("Product")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
Ok(MotherboardInfo {
manufacturer,
model,
version: json.get("Version")
.and_then(|v| v.as_str())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty()),
})
}
/// 获取主板信息(非 Windows 平台返回空)
#[tauri::command]
#[cfg(not(target_os = "windows"))]
pub async fn get_motherboard_info() -> Result<MotherboardInfo, String> {
Ok(MotherboardInfo {
manufacturer: None,
model: None,
version: None,
})
}