Files
win-softmgr/src-tauri/src/lib.rs
2026-03-30 19:22:11 -04:00

293 lines
11 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.
pub mod winget;
use std::fs;
use std::process::{Command, Stdio};
use std::os::windows::process::CommandExt;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use tokio::sync::mpsc;
use tauri::{AppHandle, Manager, State, Emitter};
use serde::{Serialize, Deserialize};
use winget::{Software, list_all_software, list_updates, ensure_winget_dependencies};
use regex::Regex;
#[derive(Clone, Serialize, Deserialize)]
pub struct AppSettings {
pub repo_url: String,
}
#[derive(Clone, Serialize, Deserialize)]
pub struct EssentialsRepo {
pub version: String,
pub essentials: Vec<Software>,
}
impl Default for AppSettings {
fn default() -> Self {
Self {
repo_url: "https://karlblue.github.io/winget-repo".to_string(),
}
}
}
struct AppState {
install_tx: mpsc::Sender<String>,
}
#[derive(Clone, Serialize)]
pub struct LogPayload {
pub id: String,
pub timestamp: String,
pub command: String,
pub output: String,
pub status: String,
}
pub fn emit_log(handle: &AppHandle, id: &str, command: &str, output: &str, status: &str) {
let now = chrono::Local::now().format("%H:%M:%S").to_string();
let _ = handle.emit("log-event", LogPayload {
id: id.to_string(),
timestamp: now,
command: command.to_string(),
output: output.to_string(),
status: status.to_string(),
});
}
fn get_settings_path(app: &AppHandle) -> PathBuf {
let app_data_dir = app.path().app_data_dir().unwrap_or_default();
if !app_data_dir.exists() {
let _ = fs::create_dir_all(&app_data_dir);
}
app_data_dir.join("settings.json")
}
fn get_essentials_path(app: &AppHandle) -> PathBuf {
let app_data_dir = app.path().app_data_dir().unwrap_or_default();
if !app_data_dir.exists() {
let _ = fs::create_dir_all(&app_data_dir);
}
app_data_dir.join("setup-essentials.json")
}
#[tauri::command]
fn get_settings(app: AppHandle) -> AppSettings {
let path = get_settings_path(&app);
if !path.exists() {
let default_settings = AppSettings::default();
let _ = fs::write(&path, serde_json::to_string_pretty(&default_settings).unwrap());
return default_settings;
}
let content = fs::read_to_string(path).unwrap_or_default();
serde_json::from_str(&content).unwrap_or_default()
}
#[tauri::command]
fn save_settings(app: AppHandle, settings: AppSettings) -> Result<(), String> {
let path = get_settings_path(&app);
let content = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
fs::write(path, content).map_err(|e| e.to_string())
}
#[tauri::command]
async fn sync_essentials(app: AppHandle) -> Result<bool, String> {
let settings = get_settings(app.clone());
let url = format!("{}/setup-essentials.json", settings.repo_url.trim_end_matches('/'));
emit_log(&app, "sync-essentials", "Syncing Essentials", &format!("Downloading from {}...", url), "info");
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()
.map_err(|e| e.to_string())?;
match client.get(&url).send().await {
Ok(response) => {
if response.status().is_success() {
let content = response.text().await.map_err(|e| e.to_string())?;
// 验证 JSON 格式(新格式:{ version: string, essentials: Vec<Software> }
let validation: Result<EssentialsRepo, _> = serde_json::from_str(&content);
if validation.is_ok() {
let path = get_essentials_path(&app);
fs::write(path, content).map_err(|e| e.to_string())?;
emit_log(&app, "sync-essentials", "Result", "Essentials list updated successfully.", "success");
Ok(true)
} else {
emit_log(&app, "sync-essentials", "Error", "Invalid JSON format from repository. Expected { version, essentials }.", "error");
Err("Invalid JSON format".to_string())
}
} else {
let err_msg = format!("HTTP Error: {}", response.status());
emit_log(&app, "sync-essentials", "Error", &err_msg, "error");
Err(err_msg)
}
}
Err(e) => {
emit_log(&app, "sync-essentials", "Skipped", &format!("Network issue: {}. Using local cache.", e), "info");
Ok(false)
}
}
}
#[tauri::command]
async fn initialize_app(app: AppHandle) -> Result<bool, String> {
let app_clone = app.clone();
tokio::task::spawn_blocking(move || {
ensure_winget_dependencies(&app_clone).map(|_| true)
}).await.unwrap_or(Err("Initialization Task Panicked".to_string()))
}
#[tauri::command]
fn get_essentials(app: AppHandle) -> Option<EssentialsRepo> {
let file_path = get_essentials_path(&app);
if !file_path.exists() {
return None;
}
let content = fs::read_to_string(file_path).unwrap_or_default();
serde_json::from_str(&content).ok()
}
#[tauri::command]
async fn get_all_software(app: AppHandle) -> Vec<Software> {
tokio::task::spawn_blocking(move || list_all_software(&app)).await.unwrap_or_default()
}
#[tauri::command]
async fn get_updates(app: AppHandle) -> Vec<Software> {
tokio::task::spawn_blocking(move || list_updates(&app)).await.unwrap_or_default()
}
#[tauri::command]
async fn install_software(id: String, state: State<'_, AppState>) -> Result<(), String> {
state.install_tx.send(id).await.map_err(|e| e.to_string())
}
#[tauri::command]
fn get_logs_history() -> Vec<LogPayload> {
vec![]
}
#[derive(Clone, Serialize)]
struct InstallProgress {
id: String,
status: String,
progress: f32,
}
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.setup(move |app| {
let handle = app.handle().clone();
let (tx, mut rx) = mpsc::channel::<String>(100);
app.manage(AppState { install_tx: tx });
tauri::async_runtime::spawn(async move {
let perc_re = Regex::new(r"(\d+)\s*%").unwrap();
let size_re = Regex::new(r"([\d\.]+)\s*[a-zA-Z]+\s*/\s*([\d\.]+)\s*[a-zA-Z]+").unwrap();
while let Some(id) = rx.recv().await {
let log_id = format!("install-{}", id);
let _ = handle.emit("install-status", InstallProgress {
id: id.clone(),
status: "installing".to_string(),
progress: 0.0,
});
emit_log(&handle, &log_id, &format!("Winget Install: {}", id), "Starting...", "info");
let id_for_cmd = id.clone();
let h = handle.clone();
let child = Command::new("winget")
.args([
"install", "--id", &id_for_cmd, "-e", "--silent",
"--accept-package-agreements", "--accept-source-agreements",
"--disable-interactivity"
])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.creation_flags(0x08000000)
.spawn();
let status_result = match child {
Ok(mut child_proc) => {
if let Some(stdout) = child_proc.stdout.take() {
let reader = BufReader::new(stdout);
for line_res in reader.split(b'\r') {
if let Ok(line_bytes) = line_res {
let line_str = String::from_utf8_lossy(&line_bytes).to_string();
let clean_line = line_str.trim();
if clean_line.is_empty() { continue; }
let mut is_progress = false;
if let Some(caps) = perc_re.captures(clean_line) {
if let Ok(p_val) = caps[1].parse::<f32>() {
let _ = h.emit("install-status", InstallProgress {
id: id_for_cmd.clone(),
status: "installing".to_string(),
progress: p_val / 100.0,
});
is_progress = true;
}
}
else if let Some(caps) = size_re.captures(clean_line) {
let current = caps[1].parse::<f32>().unwrap_or(0.0);
let total = caps[2].parse::<f32>().unwrap_or(1.0);
if total > 0.0 {
let _ = h.emit("install-status", InstallProgress {
id: id_for_cmd.clone(),
status: "installing".to_string(),
progress: (current / total).min(1.0),
});
is_progress = true;
}
}
// 净化日志:过滤进度行、单字符动画行以及退格符
if !is_progress && clean_line.chars().count() > 1 {
emit_log(&h, &log_id, "", clean_line, "info");
}
}
}
}
let exit_status = child_proc.wait().map(|s| s.success()).unwrap_or(false);
if exit_status { "success" } else { "error" }
},
Err(e) => {
emit_log(&h, &log_id, "Fatal Error", &e.to_string(), "error");
"error"
}
};
let _ = handle.emit("install-status", InstallProgress {
id: id.clone(),
status: status_result.to_string(),
progress: 1.0,
});
emit_log(&handle, &log_id, "Result", &format!("Execution finished: {}", status_result), if status_result == "success" { "success" } else { "error" });
}
});
Ok(())
})
.invoke_handler(tauri::generate_handler![
initialize_app,
get_settings,
save_settings,
sync_essentials,
get_essentials,
get_all_software,
get_updates,
install_software,
get_logs_history
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}