From f70498158a9471da4ea17cfdf77e90f75560512e Mon Sep 17 00:00:00 2001 From: funman300 Date: Sat, 18 Apr 2026 23:41:07 -0700 Subject: [PATCH] feat(settings): add Rebuild & Install self-update button Settings panel now shows the current version and a "Rebuild & Install Latest" button that: 1. git pull from the embedded source directory (CARGO_MANIFEST_DIR) 2. makepkg -sf in packaging/ 3. pkexec pacman -U (graphical polkit auth prompt) Reports the installed version on success; surfaces the failing step on error. Update runs off the UI thread so the window stays responsive. Co-Authored-By: Claude Sonnet 4.6 --- src/gui.rs | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/src/gui.rs b/src/gui.rs index f7fa58d..840e5d7 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -61,6 +61,9 @@ pub enum Message { ServiceInstall, ServiceUninstall, ServiceActionDone(Result<(), String>), + // Self-update + UpdateApp, + UpdateAppDone(Result), } struct Dashboard { @@ -87,6 +90,8 @@ struct Dashboard { proton_versions: Vec, service_busy: bool, service_status: String, + update_busy: bool, + update_status: String, } impl Dashboard { @@ -118,6 +123,8 @@ impl Dashboard { proton_versions, service_busy: false, service_status: String::new(), + update_busy: false, + update_status: String::new(), } } } @@ -570,6 +577,22 @@ fn update(state: &mut Dashboard, msg: Message) -> Task { } Task::none() } + Message::UpdateApp => { + state.update_busy = true; + state.update_status = "Pulling latest changes…".into(); + Task::perform( + async_blocking(run_self_update), + Message::UpdateAppDone, + ) + } + Message::UpdateAppDone(res) => { + state.update_busy = false; + state.update_status = match res { + Ok(ver) => format!("✓ Updated to {ver} — restart the app to apply."), + Err(e) => format!("Update failed: {e}"), + }; + Task::none() + } } } @@ -1155,6 +1178,18 @@ fn view_settings(state: &Dashboard) -> Element<'_, Message> { text(svc_status_label).size(13), row![svc_install_btn, svc_uninstall_btn,].spacing(10), text(&state.service_status).size(12), + iced::widget::horizontal_rule(1), + row![ + text(format!("Version: {}", env!("CARGO_PKG_VERSION"))).size(13), + iced::widget::horizontal_space(), + ] + .align_y(Alignment::Center), + button( + text(if state.update_busy { "Updating…" } else { "Rebuild & Install Latest" }).size(13), + ) + .on_press_maybe((!state.update_busy).then_some(Message::UpdateApp)) + .style(button::secondary), + text(&state.update_status).size(12), ] .spacing(10) .padding(20); @@ -1165,6 +1200,67 @@ fn view_settings(state: &Dashboard) -> Element<'_, Message> { .into() } +fn run_self_update() -> Result { + let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")); + if !src.exists() { + return Err(format!("Source directory not found: {}", src.display())); + } + let pkg_dir = src.join("packaging"); + + // 1. git pull + let out = std::process::Command::new("git") + .args(["-C", src.to_str().unwrap_or("."), "pull"]) + .output() + .map_err(|e| format!("git pull: {e}"))?; + if !out.status.success() { + return Err(format!("git pull failed: {}", String::from_utf8_lossy(&out.stderr))); + } + + // 2. makepkg -sf --noconfirm + let out = std::process::Command::new("makepkg") + .args(["-sf", "--noconfirm"]) + .current_dir(&pkg_dir) + .output() + .map_err(|e| format!("makepkg: {e}"))?; + if !out.status.success() { + return Err(format!("makepkg failed: {}", String::from_utf8_lossy(&out.stderr))); + } + + // 3. Find the freshly built package + let pkg = std::fs::read_dir(&pkg_dir) + .map_err(|e| e.to_string())? + .flatten() + .filter_map(|e| { + let name = e.file_name(); + let n = name.to_string_lossy().to_string(); + if n.ends_with(".pkg.tar.zst") && !n.contains("debug") { + Some(e.path()) + } else { + None + } + }) + .max_by_key(|p| p.metadata().and_then(|m| m.modified()).ok()) + .ok_or_else(|| "No .pkg.tar.zst found after makepkg".to_string())?; + + // 4. Install with pkexec (graphical auth prompt) + let out = std::process::Command::new("pkexec") + .args(["pacman", "-U", "--noconfirm", pkg.to_str().unwrap_or("")]) + .output() + .map_err(|e| format!("pkexec: {e}"))?; + if !out.status.success() { + return Err(format!("Install failed: {}", String::from_utf8_lossy(&out.stderr))); + } + + // Extract version from package filename e.g. umutray-0.1.0-4-x86_64.pkg.tar.zst + let ver = pkg + .file_name() + .and_then(|n| n.to_str()) + .and_then(|n| n.split('-').nth(1)) + .unwrap_or("?") + .to_string(); + Ok(ver) +} + pub fn run(config: &Config) -> Result<()> { let config = config.clone(); iced::application(|_: &Dashboard| String::from("umutray"), update, view)