diff --git a/frontend/src/lib/views/Updates.svelte b/frontend/src/lib/views/Updates.svelte index ec4c865..7e16970 100644 --- a/frontend/src/lib/views/Updates.svelte +++ b/frontend/src/lib/views/Updates.svelte @@ -26,6 +26,7 @@ gpu: string; reason: string; packages: string[]; + installed: boolean; } interface UpdatesStatus { pacman: PendingUpdate[]; @@ -67,16 +68,18 @@
{status.nvidia.gpu}

{status.nvidia.reason}

- + {#if status.nvidia.installed} + + {:else} + + {/if}
diff --git a/src/src/commands/nvidia.rs b/src/src/commands/nvidia.rs index cf59830..3c3b005 100644 --- a/src/src/commands/nvidia.rs +++ b/src/src/commands/nvidia.rs @@ -3,23 +3,64 @@ //! not install anything until the user clicks. use serde::Serialize; +use std::path::{Path, PathBuf}; +use tauri::AppHandle; + +use super::streaming; +use super::util::{self, command_exists, pacman_installed}; + +/// Same drop-in bos-nvidia-setup writes. hyprland.lua dofiles it only +/// when the file exists (Mesa machines have no file). +/// Hyprland 0.56 (Aquamarine): wiki requires LIBVA + GLX vendor. +/// NVD_BACKEND is the current VA-API hint. No WLR_* / GBM_BACKEND. +const NVIDIA_LUA: &str = "\ +-- Written by bos-nvidia-setup. hyprland.lua dofiles this only when it exists. +-- Hyprland 0.56 (Aquamarine) — no WLR_* variables. +-- https://wiki.hypr.land/Nvidia/ +hl.env(\"LIBVA_DRIVER_NAME\", \"nvidia\") +hl.env(\"__GLX_VENDOR_LIBRARY_NAME\", \"nvidia\") +hl.env(\"NVD_BACKEND\", \"direct\") +"; + +const HYPR_INCLUDE: &str = "\ +-- bos-nvidia-setup: optional proprietary env; no-op when the file is absent +do + local nvidia = (os.getenv(\"HOME\") or \"\") .. \"/.config/hypr/nvidia.lua\" + local f = io.open(nvidia, \"r\") + if f then + f:close() + pcall(dofile, nvidia) + end +end +"; + +const NVIDIA_PACKAGES: &[&str] = &["nvidia", "nvidia-utils"]; #[derive(Serialize, Clone)] pub struct NvidiaOffer { gpu: String, reason: String, packages: Vec, + installed: bool, } -fn offer_paths() -> Vec { +fn offer_paths() -> Vec { let home = std::env::var("HOME").unwrap_or_else(|_| "/root".into()); - let state = std::path::Path::new(&home).join(".local/state/bos"); + let state = Path::new(&home).join(".local/state/bos"); vec![ state.join("nvidia-offer.json"), state.join("nvidia-probe.json"), ] } +fn nvidia_dropin_path() -> PathBuf { + util::hypr_dir().join("nvidia.lua") +} + +fn nvidia_ready() -> bool { + nvidia_dropin_path().is_file() && NVIDIA_PACKAGES.iter().all(|p| pacman_installed(p)) +} + pub fn read_nvidia_offer() -> Option { for path in offer_paths() { if !path.is_file() { @@ -56,11 +97,12 @@ pub fn read_nvidia_offer() -> Option { .collect::>() }) .filter(|p| !p.is_empty()) - .unwrap_or_else(|| vec!["nvidia".into(), "nvidia-utils".into()]); + .unwrap_or_else(|| default_packages()); return Some(NvidiaOffer { gpu, reason, packages, + installed: nvidia_ready(), }); } return Some(generic_offer()); @@ -68,11 +110,16 @@ pub fn read_nvidia_offer() -> Option { None } +fn default_packages() -> Vec { + NVIDIA_PACKAGES.iter().map(|s| (*s).to_string()).collect() +} + fn generic_offer() -> NvidiaOffer { NvidiaOffer { gpu: "NVIDIA GPU".into(), reason: "BOS found an NVIDIA device. Install the proprietary driver only if you want it — nouveau stays otherwise.".into(), - packages: vec!["nvidia".into(), "nvidia-utils".into()], + packages: default_packages(), + installed: nvidia_ready(), } } @@ -81,8 +128,87 @@ pub fn get_nvidia_offer() -> Option { read_nvidia_offer() } +/// Install nvidia + nvidia-utils and write the Hyprland env drop-in. +/// Prefers `/usr/local/bin/bos-nvidia-setup` (ISO script) so package +/// install + env stay one path. Falls back to allowlisted pacman plus +/// the same drop-in when the script is not on this install yet. +#[tauri::command] +pub async fn nvidia_setup(app: AppHandle, session_id: String) -> bool { + let home = std::env::var("HOME").unwrap_or_default(); + if command_exists("bos-nvidia-setup") { + let ok = streaming::run_hardcoded( + app.clone(), + session_id.clone(), + "pkexec", + &["bos-nvidia-setup", "--home", &home], + ) + .await; + if ok { + streaming::emit_line(&app, &session_id, "reboot required"); + } + return ok; + } + + streaming::emit_line( + &app, + &session_id, + "bos-nvidia-setup not on PATH; installing via pacman and writing the env drop-in", + ); + let packages = default_packages(); + if !streaming::pacman_install(app.clone(), session_id.clone(), packages).await { + return false; + } + match write_nvidia_dropin() { + Ok(()) => { + streaming::emit_line(&app, &session_id, "wrote ~/.config/hypr/nvidia.lua"); + streaming::emit_line(&app, &session_id, "reboot required"); + true + } + Err(e) => { + streaming::emit_line(&app, &session_id, &format!("Error: {e}")); + false + } + } +} + +fn write_nvidia_dropin() -> Result<(), String> { + let dir = util::hypr_dir(); + std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; + let path = dir.join("nvidia.lua"); + super::config::atomic_write(&path, NVIDIA_LUA).map_err(|e| e.to_string())?; + ensure_hyprland_include()?; + Ok(()) +} + +fn include_already_present(text: &str) -> bool { + text.contains("nvidia.lua") +} + +fn ensure_hyprland_include() -> Result<(), String> { + let path = util::hypr_dir().join("hyprland.lua"); + let existing = std::fs::read_to_string(&path).unwrap_or_default(); + if include_already_present(&existing) { + return Ok(()); + } + if existing.is_empty() { + return Ok(()); + } + let mut text = existing; + if !text.ends_with('\n') { + text.push('\n'); + } + text.push('\n'); + text.push_str(HYPR_INCLUDE); + if !text.ends_with('\n') { + text.push('\n'); + } + super::config::atomic_write(&path, &text).map_err(|e| e.to_string()) +} + #[cfg(test)] mod tests { + use super::*; + #[test] fn missing_file_is_none() { // This machine's real probe path is not something the unit test @@ -103,4 +229,32 @@ mod tests { assert!(hide(r#"{"dismissed":true}"#)); assert!(!hide(r#"{"gpu":"RTX 4060"}"#)); } + + #[test] + fn dropin_has_current_wiki_env_and_no_obsolete_vars() { + assert!(NVIDIA_LUA.contains("LIBVA_DRIVER_NAME")); + assert!(NVIDIA_LUA.contains("__GLX_VENDOR_LIBRARY_NAME")); + assert!(NVIDIA_LUA.contains("NVD_BACKEND")); + assert!(!NVIDIA_LUA.contains("hl.env(\"WLR_")); + assert!(!NVIDIA_LUA.contains("hl.env(\"GBM_BACKEND")); + assert!(!NVIDIA_LUA.contains("cuda")); + } + + #[test] + fn include_snippet_is_conditional() { + assert!(HYPR_INCLUDE.contains("nvidia.lua")); + assert!(HYPR_INCLUDE.contains("io.open")); + assert!(HYPR_INCLUDE.contains("pcall(dofile")); + } + + #[test] + fn include_detects_existing_snippet() { + assert!(include_already_present("pcall(dofile, nvidia.lua)")); + assert!(!include_already_present("hl.env(\"XCURSOR_SIZE\", \"24\")")); + } + + #[test] + fn packages_are_nvidia_and_utils_only() { + assert_eq!(NVIDIA_PACKAGES, &["nvidia", "nvidia-utils"]); + } } diff --git a/src/src/lib.rs b/src/src/lib.rs index 3fe5646..da39441 100644 --- a/src/src/lib.rs +++ b/src/src/lib.rs @@ -115,6 +115,7 @@ pub fn run() { commands::breadhelp::open_breadhelp, commands::updates::get_updates_status, commands::nvidia::get_nvidia_offer, + commands::nvidia::nvidia_setup, commands::printing::get_printers, commands::printing::set_default_printer, commands::printing::add_ipp_printer,